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
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,30 @@ public struct CloseTerminal: ParsableCommand {
}
}

/// Rename a terminal tab.
public struct RenameTerminal: ParsableCommand {
public static let configuration = CommandConfiguration(commandName: "rename-terminal", abstract: "Rename a terminal tab")
@Option(name: .long, help: "Session UUID") var session: String
@Option(name: .long, help: "Terminal UUID") var terminal: String
@Argument(help: "New name") var name: String

public init() {}

public func validate() throws {
try validateUUID(session, label: "session UUID")
try validateUUID(terminal, label: "terminal UUID")
}

public func run() throws {
let result = try rpc("rename-terminal", params: [
"session_id": .string(session),
"terminal_id": .string(terminal),
"name": .string(name),
])
printJSON(result)
}
}

/// Send text to a terminal tab.
public struct Send: ParsableCommand {
public static let configuration = CommandConfiguration(commandName: "send", abstract: "Send text to a terminal")
Expand Down
1 change: 1 addition & 0 deletions Packages/CrowCLI/Sources/CrowCLILib/CrowCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public struct CrowCommand: ParsableCommand {
NewTerminal.self,
ListTerminals.self,
CloseTerminal.self,
RenameTerminal.self,
Send.self,
AddLink.self,
ListLinks.self,
Expand Down
3 changes: 3 additions & 0 deletions Packages/CrowCore/Sources/CrowCore/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ public final class AppState {
/// Called to close a non-managed terminal tab.
public var onCloseTerminal: ((UUID, UUID) -> Void)? // receives (sessionID, terminalID)

/// Called to rename a terminal tab.
public var onRenameTerminal: ((UUID, UUID, String) -> Void)? // receives (sessionID, terminalID, newName)

/// Called to add a new global terminal tab.
public var onAddGlobalTerminal: (() -> Void)?

Expand Down
3 changes: 3 additions & 0 deletions Packages/CrowUI/Sources/CrowUI/GlobalTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public struct GlobalTerminalView: View {
onClose: { id in
appState.onCloseGlobalTerminal?(id)
},
onRename: { id, name in
appState.onRenameTerminal?(AppState.globalTerminalSessionID, id, name)
},
onAdd: {
appState.onAddGlobalTerminal?()
}
Expand Down
60 changes: 58 additions & 2 deletions Packages/CrowUI/Sources/CrowUI/SessionDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ public struct SessionDetailView: View {
activeID: appState.activeTerminalID[session.id] ?? sessionTerminals[0].id,
onSelect: { id in appState.activeTerminalID[session.id] = id },
onClose: { id in appState.onCloseTerminal?(session.id, id) },
onRename: { id, name in appState.onRenameTerminal?(session.id, id, name) },
onAdd: { appState.onAddTerminal?(session.id) }
)
Divider().overlay(CorveilTheme.borderSubtle)
Expand Down Expand Up @@ -301,17 +302,44 @@ public struct TerminalTabBar: View {
let activeID: UUID
let onSelect: (UUID) -> Void
let onClose: (UUID) -> Void
let onRename: (UUID, String) -> Void
let onAdd: () -> Void

@State private var editingTerminalID: UUID?
@State private var editingName: String = ""
@FocusState private var isEditing: Bool

public var body: some View {
HStack(spacing: 0) {
ForEach(terminals) { terminal in
Button { onSelect(terminal.id) } label: {
HStack(spacing: 4) {
Image(systemName: terminal.isManaged ? "sparkles" : "terminal")
.font(.system(size: 9))
Text(terminal.name)
.font(.caption)
if editingTerminalID == terminal.id {
TextField("Name", text: $editingName)
.textFieldStyle(.plain)
.font(.caption)
.frame(minWidth: 40, maxWidth: 120)
.focused($isEditing)
.onSubmit {
commitRename(terminal.id)
}
.onExitCommand {
editingTerminalID = nil
}
.onChange(of: isEditing) { _, nowEditing in
if !nowEditing, editingTerminalID == terminal.id {
commitRename(terminal.id)
}
}
} else {
Text(terminal.name)
.font(.caption)
.onTapGesture(count: 2) {
beginEditing(terminal)
}
}
if !terminal.isManaged {
Button {
onClose(terminal.id)
Expand All @@ -331,6 +359,20 @@ public struct TerminalTabBar: View {
.background(terminal.id == activeID ? CorveilTheme.gold.opacity(0.12) : Color.clear)
}
.buttonStyle(.plain)
.contextMenu {
Button {
beginEditing(terminal)
} label: {
Label("Rename", systemImage: "pencil")
}
if !terminal.isManaged {
Button(role: .destructive) {
onClose(terminal.id)
} label: {
Label("Close", systemImage: "xmark")
}
}
}
}

Button { onAdd() } label: {
Expand All @@ -347,6 +389,20 @@ public struct TerminalTabBar: View {
}
.background(CorveilTheme.bgSurface)
}

private func beginEditing(_ terminal: SessionTerminal) {
editingTerminalID = terminal.id
editingName = terminal.name
isEditing = true
}

private func commitRename(_ terminalID: UUID) {
let trimmed = editingName.trimmingCharacters(in: .whitespaces)
if !trimmed.isEmpty {
onRename(terminalID, trimmed)
}
editingTerminalID = nil
}
}

/// Badge displaying a session's current status with appropriate color.
Expand Down
18 changes: 18 additions & 0 deletions Sources/Crow/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
appState.onCloseTerminal = { [weak service] sessionID, terminalID in
service?.closeTerminal(sessionID: sessionID, terminalID: terminalID)
}
appState.onRenameTerminal = { [weak service] sessionID, terminalID, name in
service?.renameTerminal(sessionID: sessionID, terminalID: terminalID, name: name)
}
appState.onAddGlobalTerminal = { [weak service] in
service?.addGlobalTerminal()
}
Expand Down Expand Up @@ -714,6 +717,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
return ["deleted": .bool(true)]
}
},
"rename-terminal": { @Sendable params in
guard let sessionIDStr = params["session_id"]?.stringValue,
let sessionID = UUID(uuidString: sessionIDStr),
let terminalIDStr = params["terminal_id"]?.stringValue,
let terminalID = UUID(uuidString: terminalIDStr),
let name = params["name"]?.stringValue else {
throw RPCError.invalidParams("session_id, terminal_id, and name required")
}
return try await MainActor.run {
guard capturedService.renameTerminal(sessionID: sessionID, terminalID: terminalID, name: name) else {
throw RPCError.applicationError("Terminal not found or invalid name")
}
return ["terminal_id": .string(terminalIDStr), "name": .string(name)]
}
},
"send": { @Sendable params in
guard let sessionIDStr = params["session_id"]?.stringValue,
let sessionID = UUID(uuidString: sessionIDStr),
Expand Down
14 changes: 14 additions & 0 deletions Sources/Crow/App/SessionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,20 @@ final class SessionService {
store.mutate { data in data.terminals.removeAll { $0.id == terminalID } }
}

/// Rename a terminal tab. Returns `false` if the terminal was not found or the name is invalid.
@discardableResult
func renameTerminal(sessionID: UUID, terminalID: UUID, name: String) -> Bool {
guard Validation.isValidSessionName(name),
let idx = appState.terminals[sessionID]?.firstIndex(where: { $0.id == terminalID }) else { return false }
appState.terminals[sessionID]![idx].name = name
store.mutate { data in
if let i = data.terminals.firstIndex(where: { $0.id == terminalID }) {
data.terminals[i].name = name
}
}
return true
}

// MARK: - Global Terminal Management

/// Add a new global terminal tab (not tied to any session).
Expand Down
Loading