task description auto

This commit is contained in:
Ian Gulliver
2026-10-20 08:57:37 -07:00
parent 8f3226350e
commit d530ec5edc
20 changed files with 434 additions and 342 deletions
+6
View File
@@ -19,6 +19,12 @@ typealias TaskTab = Components.Schemas.TaskTab
typealias Snapshot = Components.Schemas.Snapshot
typealias ServerStatus = Components.Schemas.Status
extension ByreTask {
var name: String {
title.isEmpty ? "New task" : title
}
}
struct ApiError: Error, CustomStringConvertible {
let status: Int
let message: String
+1 -1
View File
@@ -92,11 +92,11 @@ struct ByreCommands: Commands {
.disabled(focus == nil)
}
CommandGroup(replacing: .newItem) {
goButton("New Task", "n")
Button("Add Server…") {
Servers.shared.current = nil
openWindow(id: "main")
}
.keyboardShortcut("n")
}
PasteboardCommands()
CommandMenu("Go") {
+1 -1
View File
@@ -1598,7 +1598,7 @@ struct NoteView: View {
out += AttributedString(id)
continue
}
var a = AttributedString(task.title)
var a = AttributedString(task.name)
if task.status == .active {
a.link = AppLink.task(task.id)
a.foregroundColor = taskPalette[task.color]
+2 -2
View File
@@ -107,7 +107,7 @@ func confirmMerge(_ taskId: String, _ store: Store, _ overlays: Overlays) async
}
let title = mergeDrafts[taskId] ?? task.mergeTitle
mergeDrafts[taskId] = nil
let model = MergeModel(taskId: taskId, target: target, parentTitle: parent?.status == .active ? parent?.title : nil, coAuthors: coAuthors, title: title)
let model = MergeModel(taskId: taskId, target: target, parentTitle: parent?.status == .active ? parent?.name : nil, coAuthors: coAuthors, title: title)
model.leave = { [weak model] go in
if let model {
mergeDrafts[taskId] = model.sentTitle
@@ -346,7 +346,7 @@ private struct MergeContent: View {
@MainActor
func confirmDiscard(_ task: ByreTask, _ store: Store, _ overlays: Overlays, anchor: CGRect?) async {
if !(await overlays.showConfirm(["Discard \"\(task.title)\"?"], action: "Discard", danger: true, anchor: anchor)) {
if !(await overlays.showConfirm(["Discard \"\(task.name)\"?"], action: "Discard", danger: true, anchor: anchor)) {
return
}
do {
+1 -1
View File
@@ -57,7 +57,7 @@ private func enqueue(_ op: @escaping @MainActor () async -> Void) {
private func show(_ store: Store, _ taskId: String, _ tag: String, _ text: String) {
let line = trimmed(text).split(separator: "\n", omittingEmptySubsequences: false).first.map(String.init) ?? ""
let content = UNMutableNotificationContent()
content.title = store.findTask(taskId).map { "byre · \($0.title)" } ?? "byre"
content.title = store.findTask(taskId).map { "byre · \($0.name)" } ?? "byre"
content.body = line.count > 200 ? "\(line.prefix(199))…" : line
content.sound = .default
content.userInfo = ["task_id": taskId, "origin": store.api.origin.absoluteString]
+1 -1
View File
@@ -226,7 +226,7 @@ private struct TaskBar: View {
var body: some View {
let task = store.selectedTaskId.flatMap { store.findTask($0) }
let project = store.focusedProject()
let label = task.map { "\(project?.name ?? "") ▸ \($0.title)" } ?? project?.name ?? "No task"
let label = task.map { "\(project?.name ?? "") ▸ \($0.name)" } ?? project?.name ?? "No task"
HStack(spacing: 0) {
Color.clear.frame(maxWidth: .infinity, maxHeight: 1)
Button {
+3 -3
View File
@@ -174,10 +174,10 @@ private struct TaskRow: View {
if let parent = task.parentTaskId {
Text("↳")
.foregroundStyle(Palette.muted)
.help("Subtask of \(store.findTask(parent)?.title ?? parent)")
.help("Subtask of \(store.findTask(parent)?.name ?? parent)")
}
StateMark(state: ts, color: color)
Text(task.title)
Text(task.name)
.foregroundStyle(color)
.lineLimit(1)
.truncationMode(.tail)
@@ -190,7 +190,7 @@ private struct TaskRow: View {
.font(Fonts.font(11, weight: 700))
.foregroundStyle(Palette.accent)
.help("A turn summary you haven't read")
.accessibilityLabel("New summary from \(task.title), unread")
.accessibilityLabel("New summary from \(task.name), unread")
} else {
Text(ts.label)
.font(Fonts.font(11))
+2 -2
View File
@@ -192,7 +192,7 @@ private struct TaskLinkText: View {
Button {
store.focusTask(taskId)
} label: {
Text("\(project.map { "\($0.name) ▸ " } ?? "")\(task?.title ?? taskId)")
Text("\(project.map { "\($0.name) ▸ " } ?? "")\(task?.name ?? taskId)")
.foregroundStyle(store.taskColor(taskId) ?? Palette.accent)
}
.buttonStyle(.plain)
@@ -257,7 +257,7 @@ private struct TurnList: View {
if let project {
Text("\(project.name) ▸ ").foregroundStyle(Palette.muted)
}
Text(task?.title ?? t.taskId).foregroundStyle(color)
Text(task?.name ?? t.taskId).foregroundStyle(color)
}
.lineLimit(1)
.truncationMode(.tail)
+1 -1
View File
@@ -618,7 +618,7 @@ final class Store {
spawn("load projects") {
try await self.loadProjects()
}
case "task.created", "task.merge_title", "task.merge_queued", "task.merge_unqueued", "task.merge_after_review", "task.merge_after_review_ended", "task.merge_after_review_cancelled", "task.parent_merge_started", "task.parent_merge_stopped", "task.merge_started", "task.merge_ended", "task.discard_started", "task.merged", "task.discarded", "task.state", "task.ready", "task.discard_suggested", "task.ready_cleared", "snapshot.created":
case "task.created", "task.title", "task.merge_title", "task.merge_queued", "task.merge_unqueued", "task.merge_after_review", "task.merge_after_review_ended", "task.merge_after_review_cancelled", "task.parent_merge_started", "task.parent_merge_stopped", "task.merge_started", "task.merge_ended", "task.discard_started", "task.merged", "task.discarded", "task.state", "task.ready", "task.discard_suggested", "task.ready_cleared", "snapshot.created":
if let taskId = ev.taskId, ["task.merged", "task.discarded"].contains(ev.kind) || (ev.kind == "snapshot.created" && !["answer", "fork"].contains(str(ev.payload.value, "trigger"))) {
clearTask(self, taskId)
}
+42 -27
View File
@@ -4,7 +4,7 @@ import SwiftUI
typealias WorkflowSummary = Components.Schemas.WorkflowSummary
let goKeys: Set<String> = ["k", "p", "l", "s", "/"]
let goKeys: Set<String> = ["k", "n", "p", "l", "s", "/"]
@MainActor
func runGo(_ key: String, _ store: Store, _ overlays: Overlays) {
@@ -12,6 +12,11 @@ func runGo(_ key: String, _ store: Store, _ overlays: Overlays) {
switch key {
case "k":
overlays.openSwitcher(store)
case "n":
overlays.switcher?.close()
if let project = store.focusedProject() ?? store.projects.first {
createTask(project.id, .init(), store, overlays)
}
case "p":
store.focusTab(.project)
case "l":
@@ -31,6 +36,19 @@ func runGo(_ key: String, _ store: Store, _ overlays: Overlays) {
}
}
@MainActor
func createTask(_ projectId: String, _ body: Components.Schemas.TaskCreate, _ store: Store, _ overlays: Overlays) {
Task {
do {
let task = try await store.api.client.createTask(path: .init(projectId: projectId), body: .json(body)).created.body.json
store.composerFocus = task.id
store.focusTask(task.id, .transcript)
} catch {
overlays.showAlert(errorText(error))
}
}
}
@MainActor
func ctrlGo(_ key: String, _ store: Store, _ overlays: Overlays) -> Bool {
if terminalFocused() {
@@ -43,6 +61,7 @@ func ctrlGo(_ key: String, _ store: Store, _ overlays: Overlays) -> Bool {
private let shortcuts: [(String, [(String, String)])] = [
("Anywhere", [
("⌘K", "Switch task, or create one"),
("⌘N", "New task in the current project"),
("⌘P", "Open the Project tab"),
("⌘L", "Open the Lead tab and its composer"),
("⌘S", "Merge the right pane's task"),
@@ -53,7 +72,7 @@ private let shortcuts: [(String, [(String, String)])] = [
("Task switcher", [
("↑ ↓", "Move the highlight"),
("Enter", "Pick the highlighted row"),
("name: title", "Create the task in the project whose name starts with name"),
("name: message", "Start the task in the project whose name starts with name"),
("Esc", "Back from the workflow list to the task list"),
]),
("Composer and answers", [
@@ -101,14 +120,14 @@ private func includes(_ s: String, _ q: String) -> Bool {
q.isEmpty || s.contains(q)
}
private let switcherPlaceholder = "Go to a task, or type a title to create one…"
private let switcherPlaceholder = "Go to a task, or type a first message to start one…"
private let projectPrefix = Re(#"^([^:\s]+):\s*(.*)$"#)
struct SwitcherRow: Identifiable {
enum Kind {
case task(Project, ByreTask, Int)
case project(Project)
case newTask(Project, String)
case newTask(Project)
case startTask(Project, String)
case startWorkflow(Project, String)
case workflow(Project, String, WorkflowSummary)
@@ -199,12 +218,15 @@ final class SwitcherModel {
rows.append(SwitcherRow(id: "project:\(p.id)", kind: .project(p)))
}
let fallback = store.focusedProject() ?? store.projects.first
for project in title.isEmpty ? [] : targets ?? (fallback.map { [$0] } ?? []) {
rows.append(SwitcherRow(id: "new:\(project.id)", kind: .newTask(project, title)))
for project in targets ?? (fallback.map { [$0] } ?? []) {
if title.isEmpty {
rows.append(SwitcherRow(id: "new:\(project.id)", kind: .newTask(project)))
continue
}
rows.append(SwitcherRow(id: "go:\(project.id)", kind: .startTask(project, title)))
rows.append(SwitcherRow(id: "start:\(project.id)", kind: .startWorkflow(project, title)))
}
return (rows, "No active tasks. Type a title to create one.")
return (rows, "No active tasks. Type a first message to start one.")
}
func setText(_ value: String) {
@@ -232,10 +254,10 @@ final class SwitcherModel {
case .project(let p):
close()
store.focusProject(p.id)
case .newTask(let p, let title):
create(p.id, .init(title: title))
case .startTask(let p, let title):
create(p.id, .init(title: title, prompt: title, model: store.defaultModel))
case .newTask(let p):
create(p.id, .init())
case .startTask(let p, let message):
create(p.id, .init(prompt: message, model: store.defaultModel))
case .startWorkflow(let p, let title):
pickWorkflow(p, title)
case .workflow(let p, let title, let w):
@@ -271,14 +293,7 @@ final class SwitcherModel {
private func create(_ projectId: String, _ body: Components.Schemas.TaskCreate) {
close()
Task {
do {
let task = try await store.api.client.createTask(path: .init(projectId: projectId), body: .json(body)).created.body.json
go(task.id)
} catch {
overlays.showAlert(errorText(error))
}
}
createTask(projectId, body, store, overlays)
}
private func pickWorkflow(_ project: Project, _ title: String) {
@@ -420,7 +435,7 @@ private struct SwitcherRowView: View {
.font(Fonts.font(11, weight: 700))
.foregroundStyle(Palette.accent)
.help("A turn summary you haven't read")
.accessibilityLabel("New summary from \(task.title), unread")
.accessibilityLabel("New summary from \(task.name), unread")
} else {
Text(ts.label)
.font(Fonts.font(11))
@@ -435,22 +450,22 @@ private struct SwitcherRowView: View {
.truncationMode(.tail)
.frame(maxWidth: .infinity, alignment: .leading)
state("project")
case .newTask(let p, let title):
Text(created([("New task in ", nil), (p.name, Palette.muted), (": \(title)", nil)]))
case .newTask(let p):
Text(created([("New task in ", nil), (p.name, Palette.muted)]))
.foregroundStyle(Palette.accent)
.lineLimit(1)
.truncationMode(.tail)
.frame(maxWidth: .infinity, alignment: .leading)
state(store.defaultModel)
.help("Model for the first message")
case .startTask(let p, let title):
Text(created([("Start in ", nil), (p.name, Palette.muted), (": \(title)", nil)]))
case .startTask(let p, let message):
Text(created([("Start in ", nil), (p.name, Palette.muted), (": \(message)", nil)]))
.foregroundStyle(Palette.accent)
.lineLimit(1)
.truncationMode(.tail)
.frame(maxWidth: .infinity, alignment: .leading)
state(store.defaultModel)
.help("The title is the agent's first message")
.help("The agent's first message; the agent names the task")
case .startWorkflow(let p, let title):
Text(created([("Start workflow… in ", nil), (p.name, Palette.muted), (": \(title)", nil)]))
.foregroundStyle(Palette.accent)
@@ -486,9 +501,9 @@ private struct SwitcherRowView: View {
private func taskTitle(_ project: Project, _ task: ByreTask, _ color: Color) -> AttributedString {
var parts: [(String, Color?)] = [(project.name, Palette.muted), (" ", nil)]
if let parent = task.parentTaskId {
parts.append(("\(store.findTask(parent)?.title ?? parent) ↳ ", Palette.muted))
parts.append(("\(store.findTask(parent)?.name ?? parent) ↳ ", Palette.muted))
}
parts.append((task.title, color))
parts.append((task.name, color))
return created(parts)
}
+3
View File
@@ -758,6 +758,9 @@ final class TranscriptBuilder {
case "task.parent_merge_stopped":
return note([.text("the merge into the parent task didn't start")], .error)
case "task.title":
return note([.text("\(ev.actor) set the task title to \"\(jsString(p["title"]))\"")])
case "task.merge_title":
return note([.text("\(ev.actor) set the merge commit title to \"\(jsString(p["merge_title"]))\"")])