Make the composer wrap and grow, and surface Launch at Login

Long prompts hit a single-line field before; the composer is now an
NSTextView that wraps and grows up to seven lines, keeps Return as send,
and uses Shift+Return for a newline. Launch at Login already existed in
the menu bar icon's right-click menu, so it is now also a toggle in the
sync panel where it is easier to find.
This commit is contained in:
2026-10-16 22:28:16 +08:00
parent 569e56dbba
commit a09f7b3196
4 changed files with 199 additions and 17 deletions
+28 -16
View File
@@ -7,6 +7,8 @@ struct PanelView: View {
@ObservedObject private var sync = SyncService.shared
@State private var composer = ""
@State private var composerFocused = false
@State private var composerHeight: CGFloat = ComposerView.oneLineHeight
@State private var search = ""
@State private var showSearch = false
@State private var showSectionSwitcher = false
@@ -19,7 +21,7 @@ struct PanelView: View {
@State private var copiedID: UUID?
@State private var accessibilityGranted = AXIsProcessTrusted()
private enum Field: Hashable { case composer, search, newWorkspace }
private enum Field: Hashable { case search, newWorkspace }
@FocusState private var focusedField: Field?
var body: some View {
@@ -47,10 +49,10 @@ struct PanelView: View {
.overlay(newWorkspaceModal)
.onDrop(of: [UTType.image.identifier, UTType.fileURL.identifier], isTargeted: nil, perform: handleDrop)
.onReceive(NotificationCenter.default.publisher(for: .promptPanelDidShow)) { _ in
focusedField = .composer
composerFocused = true
accessibilityGranted = AXIsProcessTrusted()
}
.onAppear { focusedField = .composer }
.onAppear { composerFocused = true }
}
// MARK: - Header
@@ -120,7 +122,7 @@ struct PanelView: View {
.onExitCommand {
search = ""
showSearch = false
focusedField = .composer
composerFocused = true
}
if !search.isEmpty {
Button { search = "" } label: { Image(systemName: "xmark.circle.fill") }
@@ -473,6 +475,12 @@ struct PanelView: View {
.font(.caption)
.foregroundStyle(.secondary)
}
Divider()
Toggle("Launch at Login", isOn: Binding(
get: { LoginItem.isEnabled },
set: { _ in _ = LoginItem.toggle() }
))
.font(.callout)
if let error = sync.lastError {
Text(error)
.font(.caption)
@@ -548,7 +556,7 @@ struct PanelView: View {
private func dismissNewWorkspace() {
showNewWorkspace = false
newWorkspaceName = ""
focusedField = .composer
composerFocused = true
}
// MARK: - Selection
@@ -615,28 +623,32 @@ struct PanelView: View {
// MARK: - Composer
private var composerField: some View {
HStack(spacing: 8) {
TextField("Add a note or prompt (\(store.activeSection.name))", text: $composer)
.textFieldStyle(.plain)
.focused($focusedField, equals: .composer)
.onSubmit(submit)
.onExitCommand {
// Esc clears a selection first, and only hides the panel
// once there is nothing selected.
HStack(alignment: .bottom, spacing: 8) {
ComposerView(
text: $composer,
isFocused: $composerFocused,
height: $composerHeight,
placeholder: "Add a note or prompt (\(store.activeSection.name))",
onSubmit: submit,
onEscape: {
if selection.isEmpty {
PanelController.shared.hide()
} else {
clearSelection()
}
}
)
.frame(maxWidth: .infinity)
.frame(height: composerHeight)
if !composer.isEmpty {
Button(action: submit) { Image(systemName: "arrow.up.circle.fill") }
.buttonStyle(.plain)
.foregroundStyle(Color.accentColor)
.help("Add (Return; Shift+Return for a new line)")
}
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.padding(.vertical, 8)
}
private func submit() {
@@ -663,7 +675,7 @@ struct PanelView: View {
withAnimation(.easeInOut(duration: 0.15)) { showSectionSwitcher.toggle() }
}
.keyboardShortcut("k", modifiers: .command)
Button("") { focusedField = .composer }
Button("") { composerFocused = true }
.keyboardShortcut("n", modifiers: .command)
Button("") {
withAnimation(.easeInOut(duration: 0.15)) { showSync.toggle() }
@@ -739,7 +751,7 @@ struct PanelView: View {
focusedField = .search
} else {
search = ""
focusedField = .composer
composerFocused = true
}
}