Files
scarf/scarf/scarf/Features/Projects/Views/ProjectKanbanTab.swift
Alan Wizemann adcc984091 feat(kanban): full read/write board with per-project tenants
Lifts Scarf's Kanban surface from the v2.6 read-only list to a
drag-and-drop board with the complete Hermes v0.12 mutation surface
wired up, plus per-project boards bound to a Scarf-minted tenant slug
and a read-only board on iOS.

Why now: the v2.6 list was a placeholder shipped while upstream Kanban
collab was still mid-rework. v0.12 stabilized the 27-verb CLI; this
release makes Scarf a real GUI client for it. Driving real tasks
end-to-end exposed and closed a connected bug pattern (claim vs
dispatch, silent skipped_unassigned, integer-vs-ISO timestamps,
parser-leaked "(no" sentinel) that would have shipped as latent UX
papercuts otherwise.

ScarfCore: KanbanService actor (Sendable, pure I/O) wrapping every
verb; KanbanTenantReader cross-platform manifest projection; eight
new model types (TaskDetail, Comment, Event, Run, Stats, Assignee,
CreateRequest, Filters); KanbanError; pure transition planner that
maps drag-drop column changes to verb sequences, tested against
canonical Hermes JSON fixtures.

Mac: KanbanBoardView orchestrator with five-column drag-drop layout,
optimistic-merge state, KanbanInspectorPane side-pane (Comments /
Events / Runs / Log tabs, Log streams worker stdout every 2s while
running), inline assignee picker, health banner for unassigned and
last-failed-run states. New Task sheet defaults to active profile
and auto-fires kanban dispatch on submit. Sidebar moved Kanban from
Manage to Monitor. Read-only KanbanListView preserved as Board|List
toggle for narrow windows / accessibility.

Per-project: DashboardTab.kanban tab on every project gated on
hasKanban; KanbanTenantResolver mints scarf:<slug> tenants on first
interaction and persists to .scarf/manifest.json (immutable across
rename); ProjectAgentContextService surfaces the tenant in the
AGENTS.md scarf-managed block so agents pass --tenant <slug> on
kanban create. New kanban_summary dashboard widget; vocabulary
mirrored in tools/widget-schema.json and site/widgets.js.

iOS: read-only board on the project tab via paged single-column
Picker, modal detail sheet with Comments / Events / Runs. Mutations
+ drag-drop deferred to v2.8.

Tests: 19 new pure-logic tests covering decoding, planner verb
mapping, argv assembly, glance string formatting, and parser
rejection of the kanban assignees empty-state sentinel. All 348
ScarfCore tests pass.

Constraints documented in CLAUDE.md: no within-column reorder
(Hermes has no update --priority verb); no live watch streaming
yet (5s polling for board, 2s for log); no bulk re-tag for legacy
NULL-tenant tasks. Pre-v0.12 Hermes hosts gracefully hide the
surface end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:59:21 +02:00

69 lines
2.5 KiB
Swift

import SwiftUI
import ScarfCore
import ScarfDesign
/// Per-project Kanban tab. Wraps `KanbanBoardView` with the project's
/// tenant pre-applied + the workspace pre-pinned to the project
/// directory. On first appearance it mints the project's
/// `scarf:<slug>` tenant if one isn't already on disk.
///
/// Capability-gated by `HermesCapabilities.hasKanban` upstream this
/// view is only added to the project tab list when v0.12+ is detected.
struct ProjectKanbanTab: View {
@Environment(\.serverContext) private var serverContext
let project: ProjectEntry
@State private var resolvedTenant: String?
@State private var resolveError: String?
var body: some View {
Group {
if let tenant = resolvedTenant {
KanbanBoardView(
context: serverContext,
tenantFilter: tenant,
projectPath: project.path,
projectName: project.name
)
} else if let error = resolveError {
VStack(spacing: ScarfSpace.s3) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 32))
.foregroundStyle(ScarfColor.warning)
Text("Couldn't set up the project's Kanban tenant.")
.scarfStyle(.headline)
Text(error)
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.foregroundMuted)
.multilineTextAlignment(.center)
Button("Retry") {
resolveError = nil
resolveTenant()
}
.buttonStyle(ScarfSecondaryButton())
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
} else {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.task(id: project.id) {
resolveTenant()
}
}
private func resolveTenant() {
let resolver = KanbanTenantResolver(context: serverContext)
// Always-mint behaviour: even if the project board is empty
// and the user hasn't created a task yet, the tenant is
// pre-allocated so AGENTS.md surfaces it on the next chat.
do {
resolvedTenant = try resolver.resolveOrMint(for: project)
} catch {
resolveError = error.localizedDescription
}
}
}