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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Added capability of changing the profile storage directory and sharing a single directory with several profiles
- Fixed drag-and-drop into text fields: files are now inserted at the text caret position instead of where the mouse was dropped
- Fixed Accept/Decline and Yes/No button colors to respect the IDE Look and Feel theme (dark themes no longer show clashing hardcoded colors)
- Added session management when opening a session from the toolbar and when closing a tab via the Close button
Expand Down
Binary file modified docs/screenshots/profiles-tab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 15 additions & 2 deletions docs/user-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,21 @@ Named profiles are stored under a common base directory (default: `~/.netbeans/c

The base directory only affects newly created profiles. To move existing profiles to a new location, manually move their subdirectories to the new location first, then update the directory via the **New Profiles Directory** field (click **Change…**).

The read-only **Profile Storage Directory** field shows the storage path for the selected profile. For the Default profile it shows `~/.claude (not overridden)`.
The **Profile Storage Directory** field shows where Claude Code stores its configuration
for the selected profile. For the **Default** profile it shows `~/.claude (not overridden)`
and the buttons are disabled. For named profiles:

- **Computed path** (default) — `<profilesDir>/<profile-name>`. The field shows the path
with no suffix.
- **Custom path** — any directory you choose. The field shows the path with a `(custom)` suffix.

| Button | Effect |
|--------|--------|
| **Change…** | Opens a directory chooser. The chosen path is stored with the profile and used as `CLAUDE_CONFIG_DIR` when a session starts. |
| **Reset** | Clears the custom path and reverts to the computed default. Enabled only when a custom path is set. |

> **Tip:** Use a custom path to share a single Claude Code configuration directory across
> multiple profiles, or to point a profile at an existing `~/.claude`-style directory.

### Managing profiles

Expand All @@ -393,7 +407,6 @@ The read-only **Profile Storage Directory** field shows the storage path for the
| **Copy** | Duplicate the currently selected profile |
| **Rename** | Rename the currently selected profile |
| **Delete** | Delete the currently selected profile |
| **Change…** | Change the profile storage directory |

### Authentication

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.nbclaudecodegui</groupId>
<artifactId>netbeans-claude-code-gui</artifactId>
<version>0.21.22-SNAPSHOT</version>
<version>0.21.26-SNAPSHOT</version>
<packaging>nbm</packaging>
<name>Netbeans Claude Code GUI</name>
<description>NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.</description>
Expand Down
19 changes: 12 additions & 7 deletions src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java
Original file line number Diff line number Diff line change
Expand Up @@ -577,13 +577,18 @@ static Map<String, String> buildEnv(ClaudeProfile profile, java.nio.file.Path pr
env.put("TERM", "xterm-256color");

if (profile != null && !profile.isDefault()) {
// Set isolated CLAUDE_CONFIG_DIR for this profile
java.nio.file.Path configDir = ClaudeProfileStore.resolveConfigDir(profile, profilesDir);
env.put("CLAUDE_CONFIG_DIR", configDir.toAbsolutePath().toString());
try {
java.nio.file.Files.createDirectories(configDir);
} catch (java.io.IOException e) {
LOG.warning("Could not create profile config dir " + configDir + ": " + e.getMessage());
// Set isolated CLAUDE_CONFIG_DIR for this profile, unless the resolved
// directory is the same as the default (~/.claude) — in that case Claude
// already uses that directory without any override.
java.nio.file.Path configDir = ClaudeProfileStore.resolveStorageDir(profile, profilesDir);
java.nio.file.Path defaultClaudeDir = java.nio.file.Path.of(System.getProperty("user.home"), ".claude");
if (!configDir.toAbsolutePath().equals(defaultClaudeDir.toAbsolutePath())) {
env.put("CLAUDE_CONFIG_DIR", configDir.toAbsolutePath().toString());
try {
java.nio.file.Files.createDirectories(configDir);
} catch (java.io.IOException e) {
LOG.warning("Could not create profile config dir " + configDir + ": " + e.getMessage());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ public enum ProxyMode {
*/
private String extraCliArgs;

/**
* Explicit storage directory for {@code CLAUDE_CONFIG_DIR}.
* When blank, the computed path {@code <profilesDir>/<id>} is used.
* When non-blank, this exact path is used as-is.
*/
private String storageDir;

// -------------------------------------------------------------------------
// No-arg constructor (Jackson)
// -------------------------------------------------------------------------
Expand All @@ -169,6 +176,7 @@ public ClaudeProfile() {
this.modelAliases = new HashMap<>();
this.customModels = new ArrayList<>();
this.extraCliArgs = "";
this.storageDir = "";
}

// -------------------------------------------------------------------------
Expand Down Expand Up @@ -547,6 +555,35 @@ public void setExtraCliArgs(String extraCliArgs) {
this.extraCliArgs = extraCliArgs != null ? extraCliArgs : "";
}

/**
* Returns the explicit storage directory for {@code CLAUDE_CONFIG_DIR}.
* A blank value means the computed path {@code <profilesDir>/<id>} is used.
*
* @return explicit storage dir, or {@code ""} if not set
*/
public String getStorageDir() { return storageDir != null ? storageDir : ""; }

/**
* Sets the explicit storage directory for {@code CLAUDE_CONFIG_DIR}.
* Pass blank or {@code null} to fall back to the computed path.
*
* @param storageDir explicit path; {@code null} is treated as empty
*/
public void setStorageDir(String storageDir) {
this.storageDir = storageDir != null ? storageDir : "";
}

/**
* Fluent setter for {@link #setStorageDir(String)}.
*
* @param storageDir explicit path; {@code null} clears the override
* @return this profile instance
*/
public ClaudeProfile withStorageDir(String storageDir) {
setStorageDir(storageDir);
return this;
}

// -------------------------------------------------------------------------
// Object
// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,23 +206,39 @@ private static String profileNames(List<ClaudeProfile> profiles) {
}

/**
* Returns the {@code CLAUDE_CONFIG_DIR} path for a non-Default profile.
* Returns the storage directory (used as {@code CLAUDE_CONFIG_DIR}) for a
* non-Default profile.
*
* <p>The path is {@code <profilesDir>/<profile.getId()>/}.
* <p>If the profile has an explicit {@link ClaudeProfile#getStorageDir()} set,
* that path is returned as-is. Otherwise the computed path
* {@code <profilesDir>/<profile.getId()>} is used.
*
* @param profile the profile (must not be Default)
* @param profilesDir base directory for profile config dirs
* @param profile the profile (must not be Default)
* @param profilesDir base directory for per-profile config dirs; used only
* when no explicit path is set
* @return absolute path to this profile's config directory
* @throws IllegalArgumentException if {@code profile.isDefault()} is {@code true}
*/
public static Path resolveConfigDir(ClaudeProfile profile, Path profilesDir) {
public static Path resolveStorageDir(ClaudeProfile profile, Path profilesDir) {
if (profile.isDefault()) {
throw new IllegalArgumentException(
"Default profile has no isolated config dir; caller must not set CLAUDE_CONFIG_DIR.");
}
String explicit = profile.getStorageDir();
if (explicit != null && !explicit.isBlank()) {
return Path.of(explicit);
}
return profilesDir.resolve(profile.getId());
}

/**
* @deprecated Use {@link #resolveStorageDir(ClaudeProfile, Path)} instead.
*/
@Deprecated
public static Path resolveConfigDir(ClaudeProfile profile, Path profilesDir) {
return resolveStorageDir(profile, profilesDir);
}

/**
* Returns the settings directory for any profile, including Default.
* Default profile maps to {@code ~/.claude/}; named profiles map to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,12 @@ public final class ClaudeProfilesPanel extends JPanel {
private JButton renameButton;
/** Button to delete the current profile. */
private JButton deleteButton;
/** Field showing the configuration directory for the selected profile. */
private JTextField configDirField;
/** Field showing the storage directory for the selected profile. */
private JTextField storageDirField;
/** Button to set a custom storage directory for the selected profile. */
private JButton changeStorageDirButton;
/** Button to reset the storage directory to the computed default. */
private JButton resetStorageDirButton;
/** Field for extra CLI arguments passed to the claude process. */
private JTextField extraCliArgsField;

Expand Down Expand Up @@ -224,13 +228,19 @@ private void initComponents() {
mainPanel.add(profileRow, rowGbc);
row++;

// --- Config directory (read-only) ---
// --- Storage directory row ---
mainPanel.add(new JLabel("Profile Storage Directory:"), gbc(0, row, false));
configDirField = new JTextField(40);
configDirField.setEditable(false);
GridBagConstraints cfgGbc = gbcFill(1, row);
cfgGbc.gridwidth = 2;
mainPanel.add(configDirField, cfgGbc);
storageDirField = new JTextField(40);
storageDirField.setEditable(false);
mainPanel.add(storageDirField, gbcFill(1, row));
JPanel storageDirButtons = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
changeStorageDirButton = new JButton("Change\u2026");
changeStorageDirButton.addActionListener(e -> onChangeStorageDir());
resetStorageDirButton = new JButton("Reset");
resetStorageDirButton.addActionListener(e -> onResetStorageDir());
storageDirButtons.add(changeStorageDirButton);
storageDirButtons.add(resetStorageDirButton);
mainPanel.add(storageDirButtons, gbc(2, row, false));
row++;

// --- Connection type section ---
Expand Down Expand Up @@ -626,13 +636,8 @@ private void loadProfileIntoForm(ClaudeProfile p) {
renameButton.setEnabled(!def);
deleteButton.setEnabled(!def);

// Config directory
if (def) {
configDirField.setText("~/.claude (not overridden)");
} else {
Path profilesDir = ClaudeCodePreferences.getProfilesDir();
configDirField.setText(ClaudeProfileStore.resolveConfigDir(p, profilesDir).toAbsolutePath().toString());
}
// Storage directory
refreshStorageDirField(p);

// Connection type
ClaudeProfile.ConnectionType ct = p.computeConnectionType();
Expand Down Expand Up @@ -813,6 +818,46 @@ private void onChangeProfilesDir() {
}
}

private void onChangeStorageDir() {
ClaudeProfile p = currentFormProfile;
if (p == null || p.isDefault()) return;
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
String current = p.getStorageDir();
if (!current.isBlank()) {
chooser.setCurrentDirectory(new File(current));
} else {
Path profilesDir = ClaudeCodePreferences.getProfilesDir();
chooser.setCurrentDirectory(ClaudeProfileStore.resolveStorageDir(p, profilesDir).toFile());
}
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
p.withStorageDir(chooser.getSelectedFile().getAbsolutePath());
refreshStorageDirField(p);
}
}

private void onResetStorageDir() {
ClaudeProfile p = currentFormProfile;
if (p == null || p.isDefault()) return;
p.withStorageDir("");
refreshStorageDirField(p);
}

private void refreshStorageDirField(ClaudeProfile p) {
if (p.isDefault()) {
storageDirField.setText("~/.claude (not overridden)");
changeStorageDirButton.setEnabled(false);
resetStorageDirButton.setEnabled(false);
} else {
Path profilesDir = ClaudeCodePreferences.getProfilesDir();
String resolved = ClaudeProfileStore.resolveStorageDir(p, profilesDir).toAbsolutePath().toString();
boolean isCustom = !p.getStorageDir().isBlank();
storageDirField.setText(isCustom ? resolved + " (custom)" : resolved);
changeStorageDirButton.setEnabled(true);
resetStorageDirButton.setEnabled(isCustom);
}
}

// -------------------------------------------------------------------------
// Profile name dialog
// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ private Path resolveClaudeConfigDir() {
ClaudeProfile p = ClaudeProfileStore.findByName(profileName);
if (p == null) return null;
return io.github.nbclaudecodegui.settings.ClaudeProfileStore
.resolveConfigDir(p, ClaudeCodePreferences.getProfilesDir());
.resolveStorageDir(p, ClaudeCodePreferences.getProfilesDir());
}

private void onBrowse() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,7 @@ public void openSwitchDialog(SessionMode initialMode) {
io.github.nbclaudecodegui.settings.ClaudeProfile profile =
io.github.nbclaudecodegui.settings.ClaudeProfileStore.findByName(profileName);
if (profile != null) {
claudeConfigDir = io.github.nbclaudecodegui.settings.ClaudeProfileStore.resolveConfigDir(
claudeConfigDir = io.github.nbclaudecodegui.settings.ClaudeProfileStore.resolveStorageDir(
profile, ClaudeCodePreferences.getProfilesDir());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ private void onStatusLabelClicked() {
if (profileName != null && !ClaudeProfile.DEFAULT_NAME.equals(profileName)) {
ClaudeProfile profile = ClaudeProfileStore.findByName(profileName);
if (profile != null) {
claudeConfigDir = ClaudeProfileStore.resolveConfigDir(
claudeConfigDir = ClaudeProfileStore.resolveStorageDir(
profile, ClaudeCodePreferences.getProfilesDir());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ void buildEnv_namedProfile_setsClaudeConfigDir() {
assertEquals(expected, env.get("CLAUDE_CONFIG_DIR"));
}

@Test
void buildEnv_namedProfile_storageDirMatchesDefault_noClaudeConfigDir() {
// If a non-default profile's storage dir is explicitly set to ~/.claude
// (same as the default), CLAUDE_CONFIG_DIR must NOT be set.
Path defaultClaudeDir = Path.of(System.getProperty("user.home"), ".claude");
ClaudeProfile p = ClaudeProfile.createNamed("SameAsDefault");
p.setStorageDir(defaultClaudeDir.toString());
Map<String, String> env = ClaudeProcess.buildEnv(p, PROFILES_DIR);
assertFalse(env.containsKey("CLAUDE_CONFIG_DIR"),
"CLAUDE_CONFIG_DIR must not be set when storage dir equals ~/.claude");
}

// -------------------------------------------------------------------------
// Auth injection
// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,33 @@ void extraCliArgs_jacksonRoundTrip() throws Exception {
assertEquals(1, loaded.size());
assertEquals("--verbose --model foo", loaded.get(0).getExtraCliArgs());
}

// -------------------------------------------------------------------------
// resolveStorageDir
// -------------------------------------------------------------------------

@Test
void resolveStorageDir_computed() {
ClaudeProfile p = ClaudeProfile.createNamed("MyProfile");
Path profilesDir = Path.of("/base/profiles");
Path result = ClaudeProfileStore.resolveStorageDir(p, profilesDir);
assertEquals(Path.of("/base/profiles/MyProfile"), result);
}

@Test
void resolveStorageDir_explicit() {
ClaudeProfile p = ClaudeProfile.createNamed("MyProfile");
p.withStorageDir("/my/custom/dir");
Path profilesDir = Path.of("/base/profiles");
Path result = ClaudeProfileStore.resolveStorageDir(p, profilesDir);
assertEquals(Path.of("/my/custom/dir"), result);
}

@Test
void resolveStorageDir_default_throws() {
ClaudeProfile def = ClaudeProfile.createDefault();
Path profilesDir = Path.of("/base/profiles");
assertThrows(IllegalArgumentException.class,
() -> ClaudeProfileStore.resolveStorageDir(def, profilesDir));
}
}
Loading
Loading