Skip to content
Open
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
27 changes: 24 additions & 3 deletions SupacodeSettingsFeature/Reducer/SettingsFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public struct SettingsFeature {
public var claudeNotificationsState = AgentHooksInstallState.checking
public var codexProgressState = AgentHooksInstallState.checking
public var codexNotificationsState = AgentHooksInstallState.checking
Comment thread
b0x42 marked this conversation as resolved.
public var kiroProgressState = AgentHooksInstallState.checking
public var kiroNotificationsState = AgentHooksInstallState.checking
public var kiroSkillState = AgentHooksInstallState.checking
/// `nil` when the settings window is closed; non-nil selects the visible section.
public var selection: SettingsSection?
public var repositorySummaries: [SettingsRepositorySummary] = []
Expand Down Expand Up @@ -161,6 +164,7 @@ public struct SettingsFeature {
@Dependency(CLISkillClient.self) private var cliSkillClient
@Dependency(ClaudeSettingsClient.self) private var claudeSettingsClient
@Dependency(CodexSettingsClient.self) private var codexSettingsClient
@Dependency(KiroSettingsClient.self) private var kiroSettingsClient
@Dependency(ArchivedWorktreeDatesClient.self) private var archivedWorktreeDatesClient
@Dependency(SystemNotificationClient.self) private var systemNotificationClient
@Dependency(\.date.now) private var now
Expand All @@ -183,21 +187,28 @@ public struct SettingsFeature {
.run { [cliSkillClient] send in
async let claude = cliSkillClient.checkInstalled(.claude)
async let codex = cliSkillClient.checkInstalled(.codex)
async let kiro = cliSkillClient.checkInstalled(.kiro)
await send(.cliSkillChecked(agent: .claude, installed: await claude))
await send(.cliSkillChecked(agent: .codex, installed: await codex))
await send(.cliSkillChecked(agent: .kiro, installed: await kiro))
},
.run { [claudeSettingsClient, codexSettingsClient] send in
.run { [claudeSettingsClient, codexSettingsClient, kiroSettingsClient] send in
async let claudeProgressInstalled = claudeSettingsClient.checkInstalled(true)
async let claudeNotificationsInstalled = claudeSettingsClient.checkInstalled(false)
async let codexProgressInstalled = codexSettingsClient.checkInstalled(true)
async let codexNotificationsInstalled = codexSettingsClient.checkInstalled(false)
async let kiroProgressInstalled = kiroSettingsClient.checkInstalled(true)
async let kiroNotificationsInstalled = kiroSettingsClient.checkInstalled(false)

await send(.agentHookChecked(.claudeProgress, installed: await claudeProgressInstalled))
await send(
.agentHookChecked(.claudeNotifications, installed: await claudeNotificationsInstalled))
await send(.agentHookChecked(.codexProgress, installed: await codexProgressInstalled))
await send(
.agentHookChecked(.codexNotifications, installed: await codexNotificationsInstalled))
await send(.agentHookChecked(.kiroProgress, installed: await kiroProgressInstalled))
await send(
.agentHookChecked(.kiroNotifications, installed: await kiroNotificationsInstalled))
}
)
)
Expand Down Expand Up @@ -372,13 +383,15 @@ public struct SettingsFeature {
case .agentHookInstallTapped(let slot):
guard !state[hookSlot: slot].isLoading else { return .none }
state[hookSlot: slot] = .installing
return .run { [claudeSettingsClient, codexSettingsClient] send in
return .run { [claudeSettingsClient, codexSettingsClient, kiroSettingsClient] send in
do {
switch slot {
case .claudeProgress: try await claudeSettingsClient.installProgress()
case .claudeNotifications: try await claudeSettingsClient.installNotifications()
case .codexProgress: try await codexSettingsClient.installProgress()
case .codexNotifications: try await codexSettingsClient.installNotifications()
case .kiroProgress: try await kiroSettingsClient.installProgress()
case .kiroNotifications: try await kiroSettingsClient.installNotifications()
}
await send(.agentHookActionCompleted(slot, .success(true)))
} catch {
Expand All @@ -389,13 +402,15 @@ public struct SettingsFeature {
case .agentHookUninstallTapped(let slot):
guard !state[hookSlot: slot].isLoading else { return .none }
state[hookSlot: slot] = .uninstalling
return .run { [claudeSettingsClient, codexSettingsClient] send in
return .run { [claudeSettingsClient, codexSettingsClient, kiroSettingsClient] send in
do {
switch slot {
case .claudeProgress: try await claudeSettingsClient.uninstallProgress()
case .claudeNotifications: try await claudeSettingsClient.uninstallNotifications()
case .codexProgress: try await codexSettingsClient.uninstallProgress()
case .codexNotifications: try await codexSettingsClient.uninstallNotifications()
case .kiroProgress: try await kiroSettingsClient.uninstallProgress()
case .kiroNotifications: try await kiroSettingsClient.uninstallNotifications()
}
await send(.agentHookActionCompleted(slot, .success(false)))
} catch {
Expand Down Expand Up @@ -583,12 +598,14 @@ extension SettingsFeature.State {
switch agent {
case .claude: claudeSkillState
case .codex: codexSkillState
case .kiro: kiroSkillState
}
}
set {
switch agent {
case .claude: claudeSkillState = newValue
case .codex: codexSkillState = newValue
case .kiro: kiroSkillState = newValue
}
}
}
Expand All @@ -600,6 +617,8 @@ extension SettingsFeature.State {
case .claudeNotifications: claudeNotificationsState
case .codexProgress: codexProgressState
case .codexNotifications: codexNotificationsState
case .kiroProgress: kiroProgressState
case .kiroNotifications: kiroNotificationsState
}
}
set {
Expand All @@ -608,6 +627,8 @@ extension SettingsFeature.State {
case .claudeNotifications: claudeNotificationsState = newValue
case .codexProgress: codexProgressState = newValue
case .codexNotifications: codexNotificationsState = newValue
case .kiroProgress: kiroProgressState = newValue
case .kiroNotifications: kiroNotificationsState = newValue
}
}
}
Expand Down
56 changes: 56 additions & 0 deletions SupacodeSettingsShared/BusinessLogic/CLISkillContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,62 @@ nonisolated enum CLISkillContent {
supacode surface split -d v -i "test" # BAD: missing -t/-s, targets your shell
```

Flags: `-w` (worktree), `-t` (tab), `-s` (surface), `-r` (repo), `-i` (input), `-d` (direction), `-n` (new ID).
Env var defaults only target your own shell session. Pass explicit IDs for created resources.
"""

// MARK: - Kiro.

// Kiro uses SKILL.md with YAML frontmatter (same as Codex).
static let kiroSkillMd = """
---
name: \(skillName)
description: \(description)
---

# Supacode CLI

Control Supacode from the terminal. The `supacode` command is available in all Supacode terminal sessions.

## CRITICAL: ID Tracking

**NEVER call `supacode tab new` or `supacode surface split` without capturing
the output.** They print the new UUID to stdout. Without it you cannot target
the resource afterward.

**NEVER omit `-t`/`-s` when targeting a created resource.** The env vars point
to your own shell, not to anything you created.

For new tabs, surface ID = tab ID.

### Correct:

```sh
TAB_ID=$(supacode tab new -i "npm start")
SPLIT_ID=$(supacode surface split -t "$TAB_ID" -s "$TAB_ID" -d v -i "npm test")
supacode surface close -t "$TAB_ID" -s "$SPLIT_ID"
supacode tab close -t "$TAB_ID"
```

### WRONG:

```sh
supacode tab new -i "npm start" # BAD: not captured
supacode surface split -d v -i "test" # BAD: missing -t/-s, targets your shell
```

## Commands

- `supacode worktree [list [-f]|focus|run|stop|archive|unarchive|delete|pin|unpin] [-w <id>]`
- `supacode tab [list [-w] [-f]|focus|new|close] [-w <id>] [-t <id>] [-i <cmd>] [-n <uuid>]`
- `supacode surface [list [-w] [-t] [-f]|focus|split|close] [-w <id>] [-t <id>] [-s <id>] [-i <cmd>] [-d h|v] [-n <uuid>]`
- `supacode repo [list | open <path> | worktree-new [-r <id>] [--branch] [--base] [--fetch]]`
- `supacode settings [<section>]`
- `supacode socket`

`list` outputs one ID per line (percent-encoded for worktrees/repos, UUIDs for tabs/surfaces).
Use these IDs directly as `-w`, `-t`, `-s`, `-r` flag values.

Flags: `-w` (worktree), `-t` (tab), `-s` (surface), `-r` (repo), `-i` (input), `-d` (direction), `-n` (new ID).
Env var defaults only target your own shell session. Pass explicit IDs for created resources.
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ nonisolated struct CLISkillInstaller {
switch agent {
case .claude: CLISkillContent.claudeSkill
case .codex: CLISkillContent.codexSkillMd
case .kiro: CLISkillContent.kiroSkillMd
}
}
}
62 changes: 62 additions & 0 deletions SupacodeSettingsShared/BusinessLogic/KiroHookSettings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Foundation

nonisolated enum KiroHookSettings {
fileprivate static let busyOn = AgentHookSettingsCommand.busyCommand(active: true)
fileprivate static let busyOff = AgentHookSettingsCommand.busyCommand(active: false)
fileprivate static let notify = AgentHookSettingsCommand.notificationCommand(agent: "kiro")

static func progressHookEntriesByEvent() throws -> [String: [JSONValue]] {
try AgentHookPayloadSupport.extractHookGroups(
from: KiroProgressPayload(),
invalidConfiguration: KiroHookSettingsError.invalidConfiguration
)
}

static func notificationHookEntriesByEvent() throws -> [String: [JSONValue]] {
try AgentHookPayloadSupport.extractHookGroups(
from: KiroNotificationPayload(),
invalidConfiguration: KiroHookSettingsError.invalidConfiguration
)
}
}

nonisolated enum KiroHookSettingsError: Error {
case invalidConfiguration
}

// MARK: - Kiro hook entry (flat format: command + timeout_ms, no type/group wrapper).

nonisolated struct KiroHookEntry: Encodable {
let command: String
let timeoutMs: Int

enum CodingKeys: String, CodingKey {
case command
case timeoutMs = "timeout_ms"
}
}

// MARK: - Progress hooks.

// Kiro uses camelCase event names ("userPromptSubmit", "stop") unlike
// Claude/Codex which use PascalCase ("UserPromptSubmit", "Stop").
private nonisolated struct KiroProgressPayload: Encodable {
let hooks: [String: [KiroHookEntry]] = [
"userPromptSubmit": [
KiroHookEntry(command: KiroHookSettings.busyOn, timeoutMs: 10_000)
],
"stop": [
KiroHookEntry(command: KiroHookSettings.busyOff, timeoutMs: 10_000)
],
]
}

// MARK: - Notification hooks.

private nonisolated struct KiroNotificationPayload: Encodable {
let hooks: [String: [KiroHookEntry]] = [
"stop": [
KiroHookEntry(command: KiroHookSettings.notify, timeoutMs: 10_000)
],
]
}
Loading