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
17 changes: 5 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ When to use a branch instance:
Standard workflow:
- If `.hack/` is missing: `hack init`
- Start services: `hack up --detach`
- Check status: `hack ps` or `hack projects status`
- Check status: `hack ps` or `hack status`
- Open app: `hack open` (use `--json` for machine parsing)
- Stop services: `hack down`

Expand Down Expand Up @@ -249,22 +249,15 @@ Docker compose notes:
- Prefer `hack` commands; they include the right files/networks.
- Use `docker compose -f .hack/docker-compose.yml exec <service> <cmd>` only if you need exec into a running container.

Sessions (tmux-based):
- Interactive picker: `hack session` (requires fzf)
- Start/attach: `hack session start <project>` (attaches if exists)
Sessions (mux-based):
- Interactive picker: `hack session` (clack picker; switches inside tmux, attaches outside)
- Start/attach: `hack session start <project>` (attaches if exists, switches if in tmux)
- Force new: `hack session start <project> --new --name agent-1`
- With infra: `hack session start <project> --up`
- List: `hack session list`
- Stop: `hack session stop <session>`
- Exec in session: `hack session exec <session> "<command>"`
- List panes: `hack session panes <session> [--pretty]`
- Capture pane output (NDJSON, defaults to active pane): `hack session capture <session> [--pretty]`
- Tail pane output (short window, defaults to active pane): `hack session tail <session> [--pretty]`
- Setup tmux: `hack setup tmux` (installs tmux if missing)

Supervisor (remote jobs):
- Use `hack supervisor` when you need long-running tasks on remote hosts, scheduled jobs, or jobs that must outlive your local machine.
- Prefer sessions for interactive tmux work; prefer supervisor for detached/background jobs.
- Setup tmux: `hack setup tmux` (adds a keybinding; requires tmux installed)

Agent setup (CLI-first):
- Cursor rules: `hack setup cursor`
Expand Down
17 changes: 5 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ When to use a branch instance:
Standard workflow:
- If `.hack/` is missing: `hack init`
- Start services: `hack up --detach`
- Check status: `hack ps` or `hack projects status`
- Check status: `hack ps` or `hack status`
- Open app: `hack open` (use `--json` for machine parsing)
- Stop services: `hack down`

Expand Down Expand Up @@ -144,22 +144,15 @@ Docker compose notes:
- Prefer `hack` commands; they include the right files/networks.
- Use `docker compose -f .hack/docker-compose.yml exec <service> <cmd>` only if you need exec into a running container.

Sessions (tmux-based):
- Interactive picker: `hack session` (requires fzf)
- Start/attach: `hack session start <project>` (attaches if exists)
Sessions (mux-based):
- Interactive picker: `hack session` (clack picker; switches inside tmux, attaches outside)
- Start/attach: `hack session start <project>` (attaches if exists, switches if in tmux)
- Force new: `hack session start <project> --new --name agent-1`
- With infra: `hack session start <project> --up`
- List: `hack session list`
- Stop: `hack session stop <session>`
- Exec in session: `hack session exec <session> "<command>"`
- List panes: `hack session panes <session> [--pretty]`
- Capture pane output (NDJSON, defaults to active pane): `hack session capture <session> [--pretty]`
- Tail pane output (short window, defaults to active pane): `hack session tail <session> [--pretty]`
- Setup tmux: `hack setup tmux` (installs tmux if missing)

Supervisor (remote jobs):
- Use `hack supervisor` when you need long-running tasks on remote hosts, scheduled jobs, or jobs that must outlive your local machine.
- Prefer sessions for interactive tmux work; prefer supervisor for detached/background jobs.
- Setup tmux: `hack setup tmux` (adds a keybinding; requires tmux installed)

Agent setup (CLI-first):
- Cursor rules: `hack setup cursor`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public enum ProjectTab: String, CaseIterable {
@MainActor
public final class DashboardModel {
public private(set) var projects: [ProjectSummary] = []
public private(set) var projectMetaById: [String: ProjectMeta] = [:]
public private(set) var daemonStatus: DaemonStatus? = nil
public private(set) var globalStatus: GlobalStatusResponse? = nil
public private(set) var runtimeOk: Bool? = nil
Expand Down Expand Up @@ -101,11 +102,13 @@ public final class DashboardModel {
lastUpdated = Date()
}

let selectedProjectForMeta = selectedProject
async let projectsTask = fetchProjects()
async let metaTask = fetchProjectMeta(for: selectedProjectForMeta)
async let daemonTask = fetchDaemonStatus()
async let globalTask = fetchGlobalStatus()

let errors = await [projectsTask, daemonTask, globalTask].compactMap { $0 }
let errors = await [projectsTask, metaTask, daemonTask, globalTask].compactMap { $0 }
if !errors.isEmpty {
errorMessage = errors.joined(separator: "\n")
}
Expand Down Expand Up @@ -155,6 +158,18 @@ public final class DashboardModel {
}
}

public func stopSession(sessionName: String) async {
let trimmed = sessionName.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
errorMessage = "Missing session name"
return
}

await runAction(message: "Stopping session…") {
try await self.client.stopSession(sessionName: trimmed)
}
}

public func showLogs(for project: ProjectSummary) {
selectedItem = .project(project.id)
if selectedProjectTab == .logs {
Expand Down Expand Up @@ -291,6 +306,19 @@ public final class DashboardModel {
}
}

private func fetchProjectMeta(for project: ProjectSummary?) async -> String? {
guard let project else { return nil }

do {
if let meta = try await client.fetchProjectMeta(projectName: project.name) {
projectMetaById[project.id] = meta
}
return nil
} catch {
return error.localizedDescription
}
}

private func fetchDaemonStatus() async -> String? {
do {
daemonStatus = try await client.daemonStatus()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ public struct DashboardView: View {
runtimeConfigured: nil,
runtimeStatus: nil,
runtime: nil,
meta: nil,
kind: .unregistered,
status: .unknown
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ final class GhosttyTerminalSession {
enum Mode {
case logs(path: String)
case shell(workingDirectory: URL)
case sessionAttach(sessionName: String, workingDirectory: URL?)
}

let project: ProjectSummary
Expand All @@ -42,10 +43,14 @@ final class GhosttyTerminalSession {
private var initialCommand: String?

var allowsInput: Bool {
if case .shell = mode {
switch mode {
case .shell:
return true
case .sessionAttach:
return true
case .logs:
return false
}
return false
}

init(project: ProjectSummary) {
Expand Down Expand Up @@ -120,7 +125,14 @@ final class GhosttyTerminalSession {
self?.handleReadableData(handle)
}
self.pty = pty
statusMessage = allowsInput ? "Shell ready" : "Streaming logs…"
switch mode {
case .shell:
statusMessage = "Shell ready"
case .logs:
statusMessage = "Streaming logs…"
case let .sessionAttach(sessionName, _):
statusMessage = "Attached: \(sessionName)"
}
flushPendingWrites()
startRefreshLoop()
} catch {
Expand Down Expand Up @@ -326,6 +338,32 @@ final class GhosttyTerminalSession {
environment: environment,
workingDirectory: workingDirectory
)
case let .sessionAttach(sessionName, workingDirectory):
let trimmed = sessionName.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
return TerminalCommand(
executableURL: URL(fileURLWithPath: "/usr/bin/env"),
arguments: ["echo", "Missing session name"],
environment: environment,
workingDirectory: workingDirectory
)
}

if let hackPath = HackCLILocator.resolveHackExecutable(in: environment) {
return TerminalCommand(
executableURL: URL(fileURLWithPath: hackPath),
arguments: ["session", "attach", trimmed],
environment: environment,
workingDirectory: workingDirectory
)
}

return TerminalCommand(
executableURL: URL(fileURLWithPath: "/usr/bin/env"),
arguments: ["hack", "session", "attach", trimmed],
environment: environment,
workingDirectory: workingDirectory
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,3 +382,4 @@ struct GlobalStatusStrip: View {
}
}
}

Loading
Loading