summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-11-26 07:54:57 -0500
committerBen Sima <ben@bensima.com>2025-11-26 07:54:57 -0500
commit60c97ba9fac9eb9298b1b448ed5494765a33be39 (patch)
treeff53762216cb038939659057ed4fe758dbf0c53e
parent30f6e16fe4fd3c9cbfcb39cd8053504ddd11167b (diff)
Use task title as commit subject, amp output as body
Fixes gitlint failures by using the pre-validated task title as the commit subject line, while preserving amp's output in the body for review context. Body lines are truncated to 72 chars for compliance.
-rw-r--r--Omni/Agent/Worker.hs31
-rw-r--r--Omni/Jr/Web.hs205
2 files changed, 214 insertions, 22 deletions
diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs
index f732436..3190bc7 100644
--- a/Omni/Agent/Worker.hs
+++ b/Omni/Agent/Worker.hs
@@ -69,7 +69,7 @@ processTask worker task = do
_ <- runFormatters repo
-- Try to commit (this runs git hooks which may fail)
- let commitMsg = formatCommitMessage output tid
+ let commitMsg = formatCommitMessage task output
AgentLog.updateActivity "Committing..."
commitResult <- tryCommit repo commitMsg
@@ -257,21 +257,15 @@ formatTask t =
where
formatDep dep = " - " <> TaskCore.depId dep <> " [" <> Text.pack (show (TaskCore.depType dep)) <> "]"
-formatCommitMessage :: Text -> Text -> Text
-formatCommitMessage ampOutput taskId =
- case Text.lines (Text.strip ampOutput) of
- [] -> "Task completed\n\nTask-Id: " <> taskId
- [subject] -> cleanSubject subject <> "\n\nTask-Id: " <> taskId
- (subject : rest) ->
- let body = Text.strip (Text.unlines (dropWhile Text.null rest))
- in if Text.null body
- then cleanSubject subject <> "\n\nTask-Id: " <> taskId
- else cleanSubject subject <> "\n\n" <> body <> "\n\nTask-Id: " <> taskId
+formatCommitMessage :: TaskCore.Task -> Text -> Text
+formatCommitMessage task ampOutput =
+ let tid = TaskCore.taskId task
+ subject = cleanSubject (TaskCore.taskTitle task)
+ body = cleanBody ampOutput
+ in if Text.null body
+ then subject <> "\n\nTask-Id: " <> tid
+ else subject <> "\n\n" <> body <> "\n\nTask-Id: " <> tid
where
- -- Clean subject line for gitlint compliance:
- -- - Remove trailing punctuation (.:!?)
- -- - Truncate to 72 chars
- -- - Capitalize first letter
cleanSubject s =
let stripped = Text.dropWhileEnd (`elem` ['.', ':', '!', '?', ' ']) s
truncated = if Text.length stripped > 72 then Text.take 69 stripped <> "..." else stripped
@@ -280,6 +274,13 @@ formatCommitMessage ampOutput taskId =
Nothing -> truncated
in capitalized
+ cleanBody :: Text -> Text
+ cleanBody output =
+ let stripped = Text.strip output
+ lns = Text.lines stripped
+ cleaned = map (Text.take 72) lns
+ in Text.intercalate "\n" cleaned
+
monitorLog :: FilePath -> Process.ProcessHandle -> IO ()
monitorLog path ph = do
waitForFile path
diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index beef8bb..3ab0998 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -15,6 +15,7 @@ module Omni.Jr.Web
where
import Alpha
+import qualified Data.List as List
import qualified Data.Text as Text
import qualified Lucid
import qualified Network.Wai.Handler.Warp as Warp
@@ -30,11 +31,14 @@ defaultPort = 8080
type API =
Get '[Lucid.HTML] HomePage
+ :<|> "ready" :> Get '[Lucid.HTML] ReadyQueuePage
:<|> "tasks" :> Get '[Lucid.HTML] TaskListPage
:<|> "tasks" :> Capture "id" Text :> Get '[Lucid.HTML] TaskDetailPage
:<|> "tasks" :> Capture "id" Text :> "status" :> ReqBody '[FormUrlEncoded] StatusForm :> PostRedirect
-newtype HomePage = HomePage ()
+data HomePage = HomePage TaskCore.TaskStats [TaskCore.Task] [TaskCore.Task]
+
+newtype ReadyQueuePage = ReadyQueuePage [TaskCore.Task]
newtype TaskListPage = TaskListPage [TaskCore.Task]
@@ -53,18 +57,195 @@ instance FromForm StatusForm where
instance Lucid.ToHtml HomePage where
toHtmlRaw = Lucid.toHtml
- toHtml (HomePage ()) =
+ toHtml (HomePage stats readyTasks recentTasks) =
+ Lucid.doctypehtml_ <| do
+ Lucid.head_ <| do
+ Lucid.title_ "Jr Dashboard"
+ Lucid.meta_ [Lucid.charset_ "utf-8"]
+ Lucid.meta_
+ [ Lucid.name_ "viewport",
+ Lucid.content_ "width=device-width, initial-scale=1"
+ ]
+ Lucid.style_ homeStyles
+ Lucid.body_ <| do
+ Lucid.h1_ "Jr Dashboard"
+
+ Lucid.div_ [Lucid.class_ "actions"] <| do
+ Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "action-btn"] "View All Tasks"
+ Lucid.a_ [Lucid.href_ "/ready", Lucid.class_ "action-btn action-btn-primary"] "View Ready Queue"
+
+ Lucid.h2_ "Task Status"
+ Lucid.div_ [Lucid.class_ "stats-grid"] <| do
+ statCard "Open" (TaskCore.openTasks stats) "badge-open"
+ statCard "In Progress" (TaskCore.inProgressTasks stats) "badge-inprogress"
+ statCard "Review" (TaskCore.reviewTasks stats) "badge-review"
+ statCard "Approved" (TaskCore.approvedTasks stats) "badge-approved"
+ statCard "Done" (TaskCore.doneTasks stats) "badge-done"
+
+ Lucid.h2_ <| do
+ "Ready Queue "
+ Lucid.a_ [Lucid.href_ "/ready", Lucid.class_ "ready-link"]
+ <| Lucid.toHtml ("(" <> tshow (length readyTasks) <> " tasks)")
+ if null readyTasks
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks ready for work."
+ else
+ Lucid.div_ [Lucid.class_ "task-list"]
+ <| traverse_ renderTaskCard (take 5 readyTasks)
+
+ Lucid.h2_ "Recent Activity"
+ if null recentTasks
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No recent tasks."
+ else
+ Lucid.div_ [Lucid.class_ "task-list"]
+ <| traverse_ renderTaskCard recentTasks
+ where
+ statCard :: (Monad m) => Text -> Int -> Text -> Lucid.HtmlT m ()
+ statCard label count badgeClass =
+ Lucid.div_ [Lucid.class_ ("stat-card " <> badgeClass)] <| do
+ Lucid.div_ [Lucid.class_ "stat-count"] (Lucid.toHtml (tshow count))
+ Lucid.div_ [Lucid.class_ "stat-label"] (Lucid.toHtml label)
+
+ renderTaskCard :: (Monad m) => TaskCore.Task -> Lucid.HtmlT m ()
+ renderTaskCard t =
+ Lucid.div_ [Lucid.class_ "task-card"] <| do
+ Lucid.div_ [Lucid.class_ "task-header"] <| do
+ Lucid.a_
+ [ Lucid.class_ "task-id",
+ Lucid.href_ ("/tasks/" <> TaskCore.taskId t)
+ ]
+ (Lucid.toHtml (TaskCore.taskId t))
+ statusBadge (TaskCore.taskStatus t)
+ Lucid.span_ [Lucid.class_ "priority"] (Lucid.toHtml (tshow (TaskCore.taskPriority t)))
+ Lucid.p_ [Lucid.class_ "task-title"] (Lucid.toHtml (TaskCore.taskTitle t))
+
+ statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
+ statusBadge status =
+ let (cls, label) = case status of
+ TaskCore.Open -> ("badge badge-open", "Open")
+ TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
+ TaskCore.Review -> ("badge badge-review", "Review")
+ TaskCore.Approved -> ("badge badge-approved", "Approved")
+ TaskCore.Done -> ("badge badge-done", "Done")
+ in Lucid.span_ [Lucid.class_ cls] label
+
+homeStyles :: Text
+homeStyles =
+ "* { box-sizing: border-box; } \
+ \body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \
+ \ margin: 0; padding: 16px; background: #f5f5f5; max-width: 900px; } \
+ \h1 { margin: 0 0 16px 0; } \
+ \h2 { margin: 24px 0 12px 0; color: #374151; font-size: 18px; } \
+ \.actions { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; } \
+ \.action-btn { display: inline-block; padding: 10px 16px; background: white; \
+ \ border: 1px solid #d1d5db; border-radius: 6px; color: #374151; \
+ \ text-decoration: none; font-size: 14px; font-weight: 500; } \
+ \.action-btn:hover { background: #f9fafb; } \
+ \.action-btn-primary { background: #0066cc; color: white; border-color: #0066cc; } \
+ \.action-btn-primary:hover { background: #0052a3; } \
+ \.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 12px; } \
+ \.stat-card { background: white; border-radius: 8px; padding: 16px; text-align: center; \
+ \ box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
+ \.stat-count { font-size: 28px; font-weight: 700; } \
+ \.stat-label { font-size: 12px; color: #6b7280; margin-top: 4px; } \
+ \.stat-card.badge-open { border-left: 4px solid #f59e0b; } \
+ \.stat-card.badge-open .stat-count { color: #92400e; } \
+ \.stat-card.badge-inprogress { border-left: 4px solid #3b82f6; } \
+ \.stat-card.badge-inprogress .stat-count { color: #1e40af; } \
+ \.stat-card.badge-review { border-left: 4px solid #8b5cf6; } \
+ \.stat-card.badge-review .stat-count { color: #6b21a8; } \
+ \.stat-card.badge-approved { border-left: 4px solid #10b981; } \
+ \.stat-card.badge-approved .stat-count { color: #065f46; } \
+ \.stat-card.badge-done { border-left: 4px solid #10b981; } \
+ \.stat-card.badge-done .stat-count { color: #065f46; } \
+ \.ready-link { font-size: 14px; color: #0066cc; text-decoration: none; } \
+ \.ready-link:hover { text-decoration: underline; } \
+ \.empty-msg { color: #6b7280; font-style: italic; } \
+ \.task-list { display: flex; flex-direction: column; gap: 8px; } \
+ \.task-card { background: white; border-radius: 8px; padding: 16px; \
+ \ box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
+ \.task-header { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 8px; } \
+ \.task-id { font-family: monospace; color: #0066cc; text-decoration: none; \
+ \ font-size: 14px; padding: 4px 0; } \
+ \.task-id:hover { text-decoration: underline; } \
+ \.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; \
+ \ font-size: 12px; font-weight: 500; } \
+ \.badge-open { background: #fef3c7; color: #92400e; } \
+ \.badge-inprogress { background: #dbeafe; color: #1e40af; } \
+ \.badge-review { background: #ede9fe; color: #6b21a8; } \
+ \.badge-approved { background: #d1fae5; color: #065f46; } \
+ \.badge-done { background: #d1fae5; color: #065f46; } \
+ \.priority { font-size: 12px; color: #6b7280; } \
+ \.task-title { font-size: 16px; margin: 0; }"
+
+instance Lucid.ToHtml ReadyQueuePage where
+ toHtmlRaw = Lucid.toHtml
+ toHtml (ReadyQueuePage tasks) =
Lucid.doctypehtml_ <| do
Lucid.head_ <| do
- Lucid.title_ "Jr Web UI"
+ Lucid.title_ "Ready Queue - Jr Web UI"
Lucid.meta_ [Lucid.charset_ "utf-8"]
Lucid.meta_
[ Lucid.name_ "viewport",
Lucid.content_ "width=device-width, initial-scale=1"
]
+ Lucid.style_ readyStyles
Lucid.body_ <| do
- Lucid.h1_ "Jr Web UI"
- Lucid.p_ <| Lucid.a_ [Lucid.href_ "/tasks"] "View Tasks"
+ Lucid.p_ <| Lucid.a_ [Lucid.href_ "/"] "← Back to Dashboard"
+ Lucid.h1_ <| do
+ "Ready Queue "
+ Lucid.span_ [Lucid.class_ "count-badge"] (Lucid.toHtml (tshow (length tasks)))
+ if null tasks
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks are ready for work."
+ else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTask tasks
+ where
+ readyStyles :: Text
+ readyStyles =
+ "* { box-sizing: border-box; } \
+ \body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \
+ \ margin: 0; padding: 16px; background: #f5f5f5; max-width: 900px; } \
+ \h1 { margin: 16px 0; } \
+ \.count-badge { background: #0066cc; color: white; padding: 4px 10px; \
+ \ border-radius: 12px; font-size: 14px; vertical-align: middle; } \
+ \.empty-msg { color: #6b7280; font-style: italic; } \
+ \.task-list { display: flex; flex-direction: column; gap: 8px; } \
+ \.task-card { background: white; border-radius: 8px; padding: 16px; \
+ \ box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
+ \.task-header { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 8px; } \
+ \.task-id { font-family: monospace; color: #0066cc; text-decoration: none; \
+ \ font-size: 14px; padding: 4px 0; } \
+ \.task-id:hover { text-decoration: underline; } \
+ \.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; \
+ \ font-size: 12px; font-weight: 500; } \
+ \.badge-open { background: #fef3c7; color: #92400e; } \
+ \.badge-inprogress { background: #dbeafe; color: #1e40af; } \
+ \.badge-review { background: #ede9fe; color: #6b21a8; } \
+ \.badge-approved { background: #d1fae5; color: #065f46; } \
+ \.badge-done { background: #d1fae5; color: #065f46; } \
+ \.priority { font-size: 12px; color: #6b7280; } \
+ \.task-title { font-size: 16px; margin: 0; }"
+
+ renderTask :: (Monad m) => TaskCore.Task -> Lucid.HtmlT m ()
+ renderTask t =
+ Lucid.div_ [Lucid.class_ "task-card"] <| do
+ Lucid.div_ [Lucid.class_ "task-header"] <| do
+ Lucid.a_
+ [ Lucid.class_ "task-id",
+ Lucid.href_ ("/tasks/" <> TaskCore.taskId t)
+ ]
+ (Lucid.toHtml (TaskCore.taskId t))
+ statusBadge (TaskCore.taskStatus t)
+ Lucid.span_ [Lucid.class_ "priority"] (Lucid.toHtml (tshow (TaskCore.taskPriority t)))
+ Lucid.p_ [Lucid.class_ "task-title"] (Lucid.toHtml (TaskCore.taskTitle t))
+
+ statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
+ statusBadge status =
+ let (cls, label) = case status of
+ TaskCore.Open -> ("badge badge-open", "Open")
+ TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
+ TaskCore.Review -> ("badge badge-review", "Review")
+ TaskCore.Approved -> ("badge badge-approved", "Approved")
+ TaskCore.Done -> ("badge badge-done", "Done")
+ in Lucid.span_ [Lucid.class_ cls] label
instance Lucid.ToHtml TaskListPage where
toHtmlRaw = Lucid.toHtml
@@ -315,10 +496,20 @@ api :: Proxy API
api = Proxy
server :: Server API
-server = homeHandler :<|> taskListHandler :<|> taskDetailHandler :<|> taskStatusHandler
+server = homeHandler :<|> readyQueueHandler :<|> taskListHandler :<|> taskDetailHandler :<|> taskStatusHandler
where
homeHandler :: Servant.Handler HomePage
- homeHandler = pure (HomePage ())
+ homeHandler = do
+ stats <- liftIO <| TaskCore.getTaskStats Nothing
+ readyTasks <- liftIO TaskCore.getReadyTasks
+ allTasks <- liftIO TaskCore.loadTasks
+ let recentTasks = take 5 <| List.sortBy (flip compare `on` TaskCore.taskUpdatedAt) allTasks
+ pure (HomePage stats readyTasks recentTasks)
+
+ readyQueueHandler :: Servant.Handler ReadyQueuePage
+ readyQueueHandler = do
+ readyTasks <- liftIO TaskCore.getReadyTasks
+ pure (ReadyQueuePage readyTasks)
taskListHandler :: Servant.Handler TaskListPage
taskListHandler = do