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
17 changes: 16 additions & 1 deletion app/Sources/TaskNxt/Features/Tabs/TabManagementView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,33 @@ private struct TabRow: View {
@Binding var errorMessage: String?
@State private var name: String = ""
@State private var isEditing = false
@FocusState private var isNameFocused: Bool

var body: some View {
HStack {
if isEditing {
TextField("Name", text: $name, onCommit: commitRename)
.textFieldStyle(.roundedBorder)
.focused($isNameFocused)
.onExitCommand { isEditing = false }
// Focus is dropped if requested in the same update that
// inserts the field (same fix as TaskRowView).
.onAppear { DispatchQueue.main.async { isNameFocused = true } }
// Clicking elsewhere commits, like pressing Enter.
.onChange(of: isNameFocused) { focused in
if !focused && isEditing { commitRename() }
}
} else {
Text(tab.name)
Spacer()
Button("Rename") {
Button {
name = tab.name
isEditing = true
} label: {
Text("Rename")
.padding(.horizontal, 6)
.padding(.vertical, 2)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.foregroundStyle(.secondary)
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/fix-tab-rename-focus/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-11
20 changes: 20 additions & 0 deletions openspec/changes/fix-tab-rename-focus/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## Context

`TabRow` in `TabManagementView.swift` toggles between `Text` + Rename button and a `TextField` using `isEditing`. `TaskRowView` solved the identical problem in `31b0bc3`: focus requested in the same update that inserts the field is dropped, so focus must be set after the field appears. See proposal.md.

## Goals / Non-Goals

**Goals:** make rename work reliably using the pattern already in `TaskRowView`.

**Non-Goals:** rename from the popover tab bar; a shared inline-edit component; changes to `AppState.renameTab`.

## Decisions

- **Mirror `TaskRowView`**: `@FocusState`, `.onAppear { DispatchQueue.main.async { focused = true } }`, `.onChange(of: focused)` to commit on blur, `.onExitCommand` to cancel. Consistent with existing code and proven for the same bug. Alternative: extract a shared view — rejected as more code for two call sites.
- **Keep empty-name handling** in `commitRename` (falls back to current name); `renameTab` unchanged.
- **Guard against double commit**: Enter sets `isEditing = false` before the blur `onChange` fires, and the blur handler only commits while `isEditing` is true. Escape also clears `isEditing` first, so the resulting blur does not commit the draft.
- **Hit area**: `.contentShape(Rectangle())` plus small padding on the Rename button label, as done in `ef50c31`.

## Risks / Trade-offs

- Focus timing is only verifiable in the running app → manual check task. The SwiftUI target can't be built in some sandboxes; verify with a real build.
20 changes: 20 additions & 0 deletions openspec/changes/fix-tab-rename-focus/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## Why

In Settings → Tabs, clicking **Rename** swaps the label for a text field that never receives keyboard focus, so it looks like plain text and swallows typing. Renaming a tab is effectively broken. Task rows had the same bug, fixed in `31b0bc3`; the tab row was missed.

## What Changes

- Focus the rename field as soon as it appears.
- Commit the rename when the field loses focus (like Enter); Escape cancels and keeps the old name.
- Make the whole **Rename** button hit-testable, not just its text glyphs.

## Capabilities

### New Capabilities

### Modified Capabilities
- `tab-management`: "Rename a tab" gains requirements on how the inline rename interaction behaves (focus, commit, cancel).

## Impact

- `app/Sources/TaskNxt/Features/Tabs/TabManagementView.swift` (`TabRow`) only. No model, persistence, or `AppState` changes.
29 changes: 29 additions & 0 deletions openspec/changes/fix-tab-rename-focus/specs/tab-management/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## MODIFIED Requirements

### Requirement: Rename a tab
The user SHALL be able to rename any existing tab. Renaming SHALL NOT affect the tab's tasks. When the user starts a rename, the name field SHALL be immediately focused and ready for typing. The rename SHALL be committed when the user presses Enter or moves focus away from the field, and SHALL be cancelled, leaving the name unchanged, when the user presses Escape. An empty name SHALL NOT replace the existing name. The entire Rename control SHALL be clickable, not only its label text.

#### Scenario: Rename preserves tasks
- **WHEN** a tab named "Work" contains tasks
- **AND** the user renames it to "Projects"
- **THEN** the tab is now labeled "Projects" and all of its tasks remain unchanged

#### Scenario: Field is focused on rename
- **WHEN** the user activates Rename on a tab
- **THEN** the name field appears with keyboard focus and typing edits the name without any extra click

#### Scenario: Focus loss commits
- **WHEN** the user edits the name and clicks elsewhere
- **THEN** the new name is saved and the row leaves edit mode

#### Scenario: Escape cancels
- **WHEN** the user edits the name and presses Escape
- **THEN** the tab keeps its previous name and the row leaves edit mode

#### Scenario: Empty name rejected
- **WHEN** the user clears the name and commits
- **THEN** the tab keeps its previous name

#### Scenario: Whole Rename control is clickable
- **WHEN** the user clicks the padding around the Rename label
- **THEN** rename starts
11 changes: 11 additions & 0 deletions openspec/changes/fix-tab-rename-focus/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## 1. Fix TabRow rename

- [x] 1.1 Add `@FocusState` to `TabRow` and focus the field in `.onAppear` via `DispatchQueue.main.async`
- [x] 1.2 Commit on focus loss (`.onChange` of focus, only while `isEditing`)
- [x] 1.3 Cancel on Escape (`.onExitCommand`) without committing the draft
- [x] 1.4 Give the Rename button a full-area hit target (`.contentShape` + padding)

## 2. Verification

- [x] 2.1 Build the `TaskNxt` target — builds cleanly with full Xcode (`DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift build`)
- [x] 2.2 Manually verify in Settings → Tabs: focus on Rename, Enter commits, click-away commits, Escape cancels, empty name keeps old name
Loading