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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,7 @@ skills/
skills-lock.json
CLAUDE.md
build-dir/

# Python bytecode from local syntax checks
__pycache__/
*.pyc
9 changes: 4 additions & 5 deletions Sources/CodeIsland/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1891,8 +1891,7 @@ final class AppState {
private nonisolated static func readModelFromTranscript(sessionId: String, cwd: String?) -> String? {
guard let cwd = cwd else { return nil }
let projectDir = cwd.claudeProjectDirEncoded()
let home = FileManager.default.homeDirectoryForCurrentUser.path
let path = "\(home)/.claude/projects/\(projectDir)/\(sessionId).jsonl"
let path = "\(ClaudeConfigPaths.projectsDir())/\(projectDir)/\(sessionId).jsonl"
guard let handle = FileHandle(forReadingAtPath: path) else { return nil }
defer { handle.closeFile() }
let chunk = handle.readData(ofLength: 32768)
Expand Down Expand Up @@ -2272,7 +2271,7 @@ final class AppState {
private nonisolated static func discoveryWatchRoots() -> [String] {
let home = FileManager.default.homeDirectoryForCurrentUser.path
let candidates: [(String, String)] = [
("claude", "\(home)/.claude/projects"),
("claude", ClaudeConfigPaths.projectsDir()),
("codex", "\(home)/.codex/sessions"),
("gemini", "\(home)/.gemini/tmp"),
("qoder", "\(home)/.qoder/projects"),
Expand Down Expand Up @@ -2835,8 +2834,8 @@ final class AppState {
let claudePids = findClaudePids(candidatePids: candidatePids)
guard !claudePids.isEmpty else { return [] }

let home = FileManager.default.homeDirectoryForCurrentUser.path
let fm = FileManager.default
let claudeProjects = ClaudeConfigPaths.projectsDir()
var results: [DiscoveredSession] = []
var seenSessionIds: Set<String> = []

Expand All @@ -2853,7 +2852,7 @@ final class AppState {
let processStart = getProcessStartTime(pid)

let projectDir = cwd.claudeProjectDirEncoded()
let projectPath = "\(home)/.claude/projects/\(projectDir)"
let projectPath = "\(claudeProjects)/\(projectDir)"
guard let files = try? fm.contentsOfDirectory(atPath: projectPath) else { continue }

// Find the most recently modified .jsonl that was written AFTER this process started
Expand Down
10 changes: 8 additions & 2 deletions Sources/CodeIsland/ConfigInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ struct ConfigInstaller {
// Claude Code — uses hook script (with bridge dispatcher + nc fallback)
CLIConfig(
name: "Claude Code", source: "claude",
configPath: ".claude/settings.json", configKey: "hooks",
configPath: "settings.json", configKey: "hooks",
format: .claude,
events: [
("UserPromptSubmit", 5, true),
Expand All @@ -218,7 +218,9 @@ struct ConfigInstaller {
],
versionedEvents: [
"PostToolUseFailure": "2.1.89",
]
],
rootOverride: { ClaudeConfigPaths.configDir() },
displayPathOverride: { ClaudeConfigPaths.displayPath(ClaudeConfigPaths.settingsPath()) }
),
// Codex — honors $CODEX_HOME (falls back to ~/.codex)
CLIConfig(
Expand Down Expand Up @@ -749,6 +751,10 @@ struct ConfigInstaller {
try? fm.removeItem(atPath: legacyBridgePath)
try? fm.removeItem(atPath: legacyHookScriptPath)

// A previous run may have created the Claude config dir after the resolution was
// memoized, so re-resolve before installing into it.
ClaudeConfigPaths.invalidateCache()

// Install hook script + bridge binary (shared by all CLIs)
installHookScript(fm: fm)
installBridgeBinary(fm: fm)
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodeIsland/DiagnosticsExporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ struct DiagnosticsExporter {
// 3. CLI config files
let home = fm.homeDirectoryForCurrentUser.path
let configs: [(source: String, dest: String)] = [
("\(home)/.claude/settings.json", "configs/claude-settings.json"),
(ClaudeConfigPaths.settingsPath(), "configs/claude-settings.json"),
("\(home)/.codex/hooks.json", "configs/codex-hooks.json"),
("\(home)/.gemini/settings.json", "configs/gemini-settings.json"),
("\(home)/.cursor/hooks.json", "configs/cursor-hooks.json"),
Expand Down
24 changes: 24 additions & 0 deletions Sources/CodeIsland/L10n.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ final class L10n: ObservableObject {
"excluded_hook_cwd_title": "Ignore Hooks From Paths",
"excluded_hook_cwd_desc": "Comma-separated substrings. Any hook event whose working directory contains one of them is silently dropped — useful for filtering out background plugins like claude-mem. Example: .claude-mem,.cache/agents",
"excluded_hook_cwd_placeholder": "e.g. .claude-mem,.cache/agents",
"claude_config_dir_title": "Claude Code Config Directory",
"claude_config_dir_desc": "Where Claude Code keeps projects/ and settings.json. Leave empty to auto-detect ($CLAUDE_CONFIG_DIR, then whichever of ~/.claude or ~/.config/claude-code actually holds your projects, preferring ~/.claude). Set this if you use a custom CLAUDE_CONFIG_DIR — a Finder-launched app does not inherit your shell environment. Restart CodeIsland after changing.",
"claude_config_dir_placeholder": "auto-detect",
"claude_config_dir_resolved": "Currently using: %@",

// Webhook forwarding
"webhook_title": "Webhook Forwarding",
Expand Down Expand Up @@ -829,6 +833,10 @@ final class L10n: ObservableObject {
"excluded_hook_cwd_title": "忽略指定路径的 Hook",
"excluded_hook_cwd_desc": "用逗号分隔的子串。任何 hook 事件的工作目录如果包含其中之一就会被静默丢弃 —— 适合过滤 claude-mem 等后台插件。示例:.claude-mem,.cache/agents",
"excluded_hook_cwd_placeholder": "例如 .claude-mem,.cache/agents",
"claude_config_dir_title": "Claude Code 配置目录",
"claude_config_dir_desc": "Claude Code 存放 projects/ 和 settings.json 的位置。留空则自动检测(先 $CLAUDE_CONFIG_DIR,再是确实包含 projects/ 的 ~/.claude,最后 ~/.config/claude-code)。如果你使用自定义的 CLAUDE_CONFIG_DIR,请在此设置——从访达启动的应用不会继承 shell 环境变量。修改后请重启 CodeIsland。",
"claude_config_dir_placeholder": "自动检测",
"claude_config_dir_resolved": "当前使用:%@",

// Webhook 转发
"webhook_title": "Webhook 转发",
Expand Down Expand Up @@ -1169,6 +1177,10 @@ final class L10n: ObservableObject {
"excluded_hook_cwd_title": "忽略指定路徑的 Hook",
"excluded_hook_cwd_desc": "以逗號分隔的子字串。任何 hook 事件的工作目錄如果包含其中之一就會被靜默丟棄 —— 適合過濾 claude-mem 等背景外掛。範例:.claude-mem,.cache/agents",
"excluded_hook_cwd_placeholder": "例如 .claude-mem,.cache/agents",
"claude_config_dir_title": "Claude Code 設定目錄",
"claude_config_dir_desc": "Claude Code 存放 projects/ 和 settings.json 的位置。留空則自動偵測(先 $CLAUDE_CONFIG_DIR,再是確實包含 projects/ 的 ~/.claude,最後 ~/.config/claude-code)。若你使用自訂的 CLAUDE_CONFIG_DIR,請在此設定——從 Finder 啟動的應用程式不會繼承 shell 環境變數。修改後請重新啟動 CodeIsland。",
"claude_config_dir_placeholder": "自動偵測",
"claude_config_dir_resolved": "目前使用:%@",

// Webhook 轉發
"webhook_title": "Webhook 轉發",
Expand Down Expand Up @@ -1509,6 +1521,10 @@ final class L10n: ObservableObject {
"excluded_hook_cwd_title": "指定パスの Hook を無視",
"excluded_hook_cwd_desc": "カンマ区切りの部分文字列。作業ディレクトリにいずれかを含む hook イベントは静かに破棄されます。claude-mem 等のバックグラウンドプラグイン除外に便利です。例:.claude-mem,.cache/agents",
"excluded_hook_cwd_placeholder": "例: .claude-mem,.cache/agents",
"claude_config_dir_title": "Claude Code 設定ディレクトリ",
"claude_config_dir_desc": "Claude Code が projects/ と settings.json を保存する場所。空欄の場合は自動検出します($CLAUDE_CONFIG_DIR、次に projects/ が実在する ~/.claude、次に ~/.config/claude-code の順)。カスタムの CLAUDE_CONFIG_DIR を使用している場合は設定してください。Finder から起動したアプリはシェルの環境変数を継承しません。変更後は CodeIsland を再起動してください。",
"claude_config_dir_placeholder": "自動検出",
"claude_config_dir_resolved": "現在の使用先: %@",

// Webhook 転送
"webhook_title": "Webhook 転送",
Expand Down Expand Up @@ -1849,6 +1865,10 @@ final class L10n: ObservableObject {
"excluded_hook_cwd_title": "지정 경로의 Hook 무시",
"excluded_hook_cwd_desc": "쉼표로 구분된 부분 문자열. 작업 디렉터리에 하나라도 포함된 hook 이벤트는 조용히 폐기됩니다. claude-mem 같은 백그라운드 플러그인 필터링에 유용합니다. 예: .claude-mem,.cache/agents",
"excluded_hook_cwd_placeholder": "예: .claude-mem,.cache/agents",
"claude_config_dir_title": "Claude Code 설정 디렉터리",
"claude_config_dir_desc": "Claude Code가 projects/ 및 settings.json을 저장하는 위치입니다. 비워 두면 자동으로 감지합니다($CLAUDE_CONFIG_DIR, 그다음 projects/가 실제로 있는 ~/.claude, 그다음 ~/.config/claude-code). 사용자 지정 CLAUDE_CONFIG_DIR을 사용하는 경우 설정하세요. Finder에서 실행된 앱은 셸 환경 변수를 상속하지 않습니다. 변경 후 CodeIsland를 다시 시작하세요.",
"claude_config_dir_placeholder": "자동 감지",
"claude_config_dir_resolved": "현재 사용 중: %@",

// Webhook 전달
"webhook_title": "Webhook 전달",
Expand Down Expand Up @@ -2189,6 +2209,10 @@ final class L10n: ObservableObject {
"excluded_hook_cwd_title": "Belirli Yolların Hook'larını Yoksay",
"excluded_hook_cwd_desc": "Virgülle ayrılmış alt dizeler. Çalışma dizini bunlardan birini içeren hook olayları sessizce yok sayılır — claude-mem gibi arka plan eklentilerini filtrelemek için kullanışlı. Örn: .claude-mem,.cache/agents",
"excluded_hook_cwd_placeholder": "örn. .claude-mem,.cache/agents",
"claude_config_dir_title": "Claude Code Yapılandırma Dizini",
"claude_config_dir_desc": "Claude Code'un projects/ ve settings.json dosyalarını sakladığı yer. Otomatik algılama için boş bırakın ($CLAUDE_CONFIG_DIR, ardından projects/ klasörünü gerçekten içeren ~/.claude, ardından ~/.config/claude-code). Özel bir CLAUDE_CONFIG_DIR kullanıyorsanız bunu ayarlayın; Finder'dan başlatılan bir uygulama kabuk ortam değişkenlerini devralmaz. Değişiklikten sonra CodeIsland'ı yeniden başlatın.",
"claude_config_dir_placeholder": "otomatik algıla",
"claude_config_dir_resolved": "Şu anda kullanılan: %@",

// Webhook iletme
"webhook_title": "Webhook İletme",
Expand Down
3 changes: 1 addition & 2 deletions Sources/CodeIsland/SessionTitleStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ enum SessionTitleStore {
guard let cwd else { return nil }

let projectDir = cwd.claudeProjectDirEncoded()
let home = FileManager.default.homeDirectoryForCurrentUser.path
let path = "\(home)/.claude/projects/\(projectDir)/\(sessionId).jsonl"
let path = "\(ClaudeConfigPaths.projectsDir())/\(projectDir)/\(sessionId).jsonl"

guard let handle = FileHandle(forReadingAtPath: path) else {
return nil
Expand Down
14 changes: 14 additions & 0 deletions Sources/CodeIsland/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ enum SettingsKey {
// Hook cwd exclusion (comma-separated substrings; cwd containing any drops the event)
static let excludedHookCwdSubstrings = "excludedHookCwdSubstrings"

// Claude Code config dir override (empty = auto-detect). Must match
// ClaudeConfigPaths.preferenceKey — that resolver is the only reader.
static let claudeConfigDir = "claude_config_dir"

// Webhook forwarding: POST hook events to an external URL
static let webhookEnabled = "webhookEnabled"
static let webhookURL = "webhookURL"
Expand Down Expand Up @@ -195,6 +199,8 @@ struct SettingsDefaults {

static let excludedHookCwdSubstrings = ""

static let claudeConfigDir = ""

static let webhookEnabled = false
static let webhookURL = ""
static let webhookEventFilter = ""
Expand Down Expand Up @@ -259,6 +265,7 @@ class SettingsManager {
SettingsKey.defaultSource: SettingsDefaults.defaultSource,
SettingsKey.autoApproveTools: SettingsDefaults.autoApproveTools,
SettingsKey.excludedHookCwdSubstrings: SettingsDefaults.excludedHookCwdSubstrings,
SettingsKey.claudeConfigDir: SettingsDefaults.claudeConfigDir,
SettingsKey.webhookEnabled: SettingsDefaults.webhookEnabled,
SettingsKey.webhookURL: SettingsDefaults.webhookURL,
SettingsKey.webhookEventFilter: SettingsDefaults.webhookEventFilter,
Expand Down Expand Up @@ -410,6 +417,13 @@ class SettingsManager {
get { defaults.string(forKey: SettingsKey.excludedHookCwdSubstrings) ?? SettingsDefaults.excludedHookCwdSubstrings }
set { defaults.set(newValue, forKey: SettingsKey.excludedHookCwdSubstrings) }
}

/// Explicit Claude Code config dir. Empty means auto-detect — see `ClaudeConfigPaths`.
/// Needed because a Finder-launched app inherits no `$CLAUDE_CONFIG_DIR`.
var claudeConfigDir: String {
get { defaults.string(forKey: SettingsKey.claudeConfigDir) ?? SettingsDefaults.claudeConfigDir }
set { defaults.set(newValue, forKey: SettingsKey.claudeConfigDir) }
}
}

// MARK: - Shortcut Actions
Expand Down
14 changes: 14 additions & 0 deletions Sources/CodeIsland/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ private struct BehaviorPage: View {
@AppStorage(SettingsKey.maxToolHistory) private var maxToolHistory = SettingsDefaults.maxToolHistory
@AppStorage(SettingsKey.autoApproveTools) private var autoApproveRaw: String = SettingsDefaults.autoApproveTools
@AppStorage(SettingsKey.excludedHookCwdSubstrings) private var excludedHookCwdSubstrings: String = SettingsDefaults.excludedHookCwdSubstrings
@AppStorage(SettingsKey.claudeConfigDir) private var claudeConfigDir: String = SettingsDefaults.claudeConfigDir
@AppStorage(SettingsKey.webhookEnabled) private var webhookEnabled: Bool = SettingsDefaults.webhookEnabled
@AppStorage(SettingsKey.webhookURL) private var webhookURL: String = SettingsDefaults.webhookURL
@AppStorage(SettingsKey.webhookEventFilter) private var webhookEventFilter: String = SettingsDefaults.webhookEventFilter
Expand Down Expand Up @@ -500,6 +501,19 @@ private struct BehaviorPage: View {
}
}

Section(l10n["claude_config_dir_title"]) {
Text(l10n["claude_config_dir_desc"])
.font(.caption)
.foregroundStyle(.secondary)
TextField(l10n["claude_config_dir_placeholder"], text: $claudeConfigDir)
.textFieldStyle(.roundedBorder)
.font(.system(size: 12, design: .monospaced))
Text(String(format: l10n["claude_config_dir_resolved"],
ClaudeConfigPaths.displayPath(ClaudeConfigPaths.configDir())))
.font(.system(size: 11, design: .monospaced))
.foregroundStyle(.secondary)
}

Section(l10n["excluded_hook_cwd_title"]) {
Text(l10n["excluded_hook_cwd_desc"])
.font(.caption)
Expand Down
Loading