diff --git a/doc-onevcat/plans/2026-06-27-foundation-model-branch-name.md b/doc-onevcat/plans/2026-06-27-foundation-model-branch-name.md index 62267af0..946b3831 100644 --- a/doc-onevcat/plans/2026-06-27-foundation-model-branch-name.md +++ b/doc-onevcat/plans/2026-06-27-foundation-model-branch-name.md @@ -207,11 +207,14 @@ When no prefix convention is detected (fresh repo), the prefix instruction is re 1. User triggers Cmd+N → `createRandomWorktreeInRepository` 2. `.run` effect loads branch refs (existing) + gathers context + calls `suggest` in parallel 3. `promptedWorktreeCreationDataLoaded` → dialog opens with `branchName: ""`, - `isSuggestingName: true` + `isSuggestingName: true`, `randomPlaceholder` pre-generated as random name 4. AI suggestion arrives → `branchNameSuggestionReceived(name)` - - If `branchName` is still empty → auto-fill input field - - Regardless of user input state → show suggestion below input field as dim hint + "Use" button + - Does NOT auto-fill input field — suggestion only shown in dim hint line below + - Hint line shows "Auto suggestion: {name}" with a "Use" button + - Hover tooltip explains the suggestion source (on-device AI, context-based) 5. `isSuggestingName = false` → loading indicator disappears, suggestion hint visible +6. User clicks "Create" → uses **effective name**: user input if non-empty, else random + placeholder. Empty input no longer blocks creation. ### Non-prompt path (auto-create without dialog) @@ -224,6 +227,9 @@ latency would violate that intent. AI naming only applies to the dialog path. Add to `State`: - `var isSuggestingName: Bool = false` - `var suggestedBranchName: String?` — stores the AI suggestion for display +- `let randomPlaceholder: String` — pre-generated random name, shown as placeholder +- Computed `effectiveBranchName: String` — returns `branchName` if non-empty, else + `randomPlaceholder`. Used by submit and path preview. Add to `Action`: - `case branchNameSuggestionReceived(String?)` — AI result arrived @@ -231,19 +237,25 @@ Add to `Action`: Reducer logic: - `branchNameSuggestionReceived(name)`: set `isSuggestingName = false`, - store `suggestedBranchName = name`. If `branchName` is empty and name is non-nil, - also set `branchName = name`. + store `suggestedBranchName = name`. Does NOT auto-fill `branchName`. - `useSuggestedBranchName`: copy `suggestedBranchName` into `branchName`. +- `createButtonTapped`: use `effectiveBranchName` instead of `branchName` for validation + and submit. Empty input is no longer an error (falls through to random placeholder). ### UI Changes in `WorktreeCreationPromptView` -**Loading state** (`isSuggestingName == true`): -- Show a subtle `ProgressView` near the text field (e.g., trailing overlay or below) +**Branch name field**: +- Placeholder shows the pre-generated random name (e.g., `bold-cat-042`) +- Empty input is allowed — placeholder name will be used on submit - Text field remains editable at all times +**Loading state** (`isSuggestingName == true`): +- Show a subtle `ProgressView` near the text field (trailing overlay) + **Suggestion hint** (`suggestedBranchName != nil`): -- Below the input field, show the suggestion in dim/secondary style -- Alongside a small "Use" button (clicking overwrites input field content) +- Below the input field: "Auto suggestion: {name}" in dim/tertiary style +- "Use" button alongside (clicking copies suggestion into input field) +- Hover tooltip: explains this is an on-device AI suggestion based on repo context - Visible regardless of whether the user has typed anything - Hidden once user submits (Create) or cancels diff --git a/supacode/Features/Repositories/Reducer/RepositoriesFeature+WorktreeCreation.swift b/supacode/Features/Repositories/Reducer/RepositoriesFeature+WorktreeCreation.swift index 6dd258b0..dd136770 100644 --- a/supacode/Features/Repositories/Reducer/RepositoriesFeature+WorktreeCreation.swift +++ b/supacode/Features/Repositories/Reducer/RepositoriesFeature+WorktreeCreation.swift @@ -146,6 +146,12 @@ extension RepositoriesFeature { globalDefaultPath: settingsFile.global.defaultWorktreeBaseDirectoryPath, repositoryOverridePath: repositorySettings.worktreeBaseDirectoryPath ) + let existingNames = Set( + repository.worktrees.map(\.name) + baseRefOptions + ) + let randomPlaceholder = + WorktreeNameGenerator.nextName(excluding: existingNames) + ?? "new-worktree" state.worktreeCreationPrompt = WorktreeCreationPromptFeature.State( repositoryID: repository.id, repositoryRootURL: repository.rootURL, @@ -157,7 +163,8 @@ extension RepositoriesFeature { fetchRemote: settingsFile.global.fetchOriginBeforeWorktreeCreation, defaultWorktreeBaseDirectory: defaultWorktreeBaseDirectory.path(percentEncoded: false), validationMessage: nil, - isSuggestingName: true + isSuggestingName: true, + randomPlaceholder: randomPlaceholder ) let branchNameSuggestionClient = branchNameSuggestionClient let repositoryName = repository.name diff --git a/supacode/Features/Repositories/Reducer/WorktreeCreationPromptFeature.swift b/supacode/Features/Repositories/Reducer/WorktreeCreationPromptFeature.swift index d1e1998a..c766894d 100644 --- a/supacode/Features/Repositories/Reducer/WorktreeCreationPromptFeature.swift +++ b/supacode/Features/Repositories/Reducer/WorktreeCreationPromptFeature.swift @@ -27,6 +27,12 @@ struct WorktreeCreationPromptFeature { var isValidating = false var isSuggestingName = false var suggestedBranchName: String? + let randomPlaceholder: String + + var effectiveBranchName: String { + let trimmed = branchName.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? randomPlaceholder : trimmed + } var automaticBaseRefLabel: String { automaticBaseRef.isEmpty ? "Automatic" : "Automatic (\(automaticBaseRef))" @@ -34,7 +40,7 @@ struct WorktreeCreationPromptFeature { /// Default leaf folder name shown as the name-override placeholder. var worktreeNamePlaceholder: String { - branchName.trimmingCharacters(in: .whitespacesAndNewlines) + effectiveBranchName } /// Live validity of the current name override, so the footer can flag an @@ -51,7 +57,7 @@ struct WorktreeCreationPromptFeature { repositoryRootURL: repositoryRootURL, nameOverride: worktreeNameOverride, pathOverride: worktreePathOverride, - branchName: branchName + branchName: effectiveBranchName ) .path(percentEncoded: false) } @@ -91,12 +97,8 @@ struct WorktreeCreationPromptFeature { return .send(.delegate(.cancel)) case .createButtonTapped: - let trimmed = state.branchName.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty else { - state.validationMessage = "Branch name required." - return .none - } - guard !trimmed.contains(where: \.isWhitespace) else { + let effective = state.effectiveBranchName + guard !effective.contains(where: \.isWhitespace) else { state.validationMessage = "Branch names can't contain spaces." return .none } @@ -111,7 +113,7 @@ struct WorktreeCreationPromptFeature { .delegate( .submit( repositoryID: state.repositoryID, - branchName: trimmed, + branchName: effective, baseRef: state.selectedBaseRef, placement: WorktreePlacementOverride( name: nameOverride.isEmpty ? nil : nameOverride, @@ -132,9 +134,6 @@ struct WorktreeCreationPromptFeature { case .branchNameSuggestionReceived(let name): state.isSuggestingName = false state.suggestedBranchName = name - if let name, state.branchName.isEmpty { - state.branchName = name - } return .none case .useSuggestedBranchName: diff --git a/supacode/Features/Repositories/Views/WorktreeCreationPromptView.swift b/supacode/Features/Repositories/Views/WorktreeCreationPromptView.swift index 307c16b8..55b73e9a 100644 --- a/supacode/Features/Repositories/Views/WorktreeCreationPromptView.swift +++ b/supacode/Features/Repositories/Views/WorktreeCreationPromptView.swift @@ -17,21 +17,28 @@ struct WorktreeCreationPromptView: View { VStack(alignment: .leading, spacing: 8) { Text("Branch name") .foregroundStyle(.secondary) - TextField("feature/my-change", text: $store.branchName) - .textFieldStyle(.roundedBorder) - .focused($isBranchFieldFocused) - .onSubmit { - store.send(.createButtonTapped) - } - .overlay(alignment: .trailing) { - if store.isSuggestingName { - ProgressView() - .controlSize(.mini) - .padding(.trailing, 6) - } + TextField( + "Branch name", + text: $store.branchName, + prompt: Text(store.randomPlaceholder) + ) + .textFieldStyle(.roundedBorder) + .focused($isBranchFieldFocused) + .onSubmit { + store.send(.createButtonTapped) + } + .overlay(alignment: .trailing) { + if store.isSuggestingName { + ProgressView() + .controlSize(.mini) + .padding(.trailing, 6) } + } if let suggested = store.suggestedBranchName { HStack(spacing: 4) { + Text("Auto suggestion: ") + .font(.footnote) + .foregroundStyle(.tertiary) Text(suggested) .font(.footnote) .monospaced() @@ -46,6 +53,10 @@ struct WorktreeCreationPromptView: View { .buttonStyle(.plain) .foregroundStyle(.tint) } + .help( + "Suggested by on-device AI based on your repository context " + + "and recent terminal activity. May not always be accurate." + ) } }