@teamclaws/teamclaw 2026.3.21 → 2026.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ui/app.js CHANGED
@@ -6,7 +6,7 @@
6
6
  let ws = null;
7
7
  let currentFilter = "all";
8
8
  let activeTab = "tasks";
9
- let teamState = { workers: [], tasks: [], messages: [], clarifications: [] };
9
+ let teamState = { workers: [], tasks: [], controllerRuns: [], messages: [], clarifications: [] };
10
10
  let selectedTaskId = null;
11
11
  let selectedTaskDetail = null;
12
12
  let selectedTaskDetailTab = "overview";
@@ -126,6 +126,25 @@
126
126
  return "#";
127
127
  }
128
128
 
129
+ function normalizeSkillList(skills) {
130
+ if (!Array.isArray(skills)) {
131
+ return [];
132
+ }
133
+ return skills
134
+ .map(function (skill) { return String(skill || "").trim(); })
135
+ .filter(Boolean);
136
+ }
137
+
138
+ function renderSkillPills(skills, className) {
139
+ const items = normalizeSkillList(skills);
140
+ if (items.length === 0) {
141
+ return "";
142
+ }
143
+ return '<div class="' + escapeHtml(className || "skill-pills") + '">' + items.map(function (skill) {
144
+ return '<span class="skill-pill">' + escapeHtml(skill) + "</span>";
145
+ }).join("") + "</div>";
146
+ }
147
+
129
148
  function renderMarkdownInline(text) {
130
149
  const codeTokens = [];
131
150
  let safe = escapeHtml(text || "");
@@ -613,6 +632,9 @@
613
632
  : null;
614
633
 
615
634
  switch (event.type) {
635
+ case "controller:run":
636
+ handleControllerRunEvent(event.data || {});
637
+ break;
616
638
  case "task:execution":
617
639
  handleTaskExecutionEvent(event.data || {});
618
640
  break;
@@ -642,12 +664,14 @@
642
664
  teamState = {
643
665
  workers: statusRes.workers || [],
644
666
  tasks: statusRes.tasks || [],
667
+ controllerRuns: statusRes.controllerRuns || [],
645
668
  messages: statusRes.messages || [],
646
669
  clarifications: statusRes.clarifications || [],
647
670
  };
648
671
 
649
672
  renderWorkers(teamState.workers);
650
673
  renderTasks(teamState.tasks);
674
+ renderControllerRuns(teamState.controllerRuns);
651
675
  renderClarifications(teamState.clarifications);
652
676
  renderMessages(teamState.messages);
653
677
  renderRoles(rolesRes.roles || []);
@@ -707,6 +731,10 @@
707
731
  const assignee = task.assignedWorkerId
708
732
  ? "Assigned to " + task.assignedWorkerId
709
733
  : (task.assignedRole ? "Role: " + task.assignedRole : "Unassigned");
734
+ const recommendedSkills = normalizeSkillList(task.recommendedSkills);
735
+ const creatorBadge = task.createdBy
736
+ ? '<span class="task-origin-badge">' + escapeHtml(task.createdBy) + "</span>"
737
+ : "";
710
738
  const note = task.progress
711
739
  ? '<div class="task-note">' + escapeHtml(task.progress).slice(0, 220) + "</div>"
712
740
  : "";
@@ -719,8 +747,9 @@
719
747
  '<div class="task-card' + liveClass + '" data-task-id="' + escapeHtml(task.id) + '" tabindex="0" role="button" aria-label="Open details for ' + escapeHtml(task.title) + '">' +
720
748
  ' <span class="task-priority ' + escapeHtml(priority) + '">' + escapeHtml(priority) + "</span>" +
721
749
  ' <div class="task-body">' +
722
- ' <div class="task-title">' + escapeHtml(task.title) + "</div>" +
750
+ ' <div class="task-title-row"><div class="task-title">' + escapeHtml(task.title) + "</div>" + creatorBadge + "</div>" +
723
751
  (task.description ? '<div class="task-desc">' + escapeHtml(task.description).slice(0, 220) + "</div>" : "") +
752
+ renderSkillPills(recommendedSkills, "skill-pills task-skill-pills") +
724
753
  note +
725
754
  ' <div class="task-meta">' +
726
755
  ' <span class="task-status-badge ' + escapeHtml(status) + '">' + escapeHtml(humanizeStatus(status)) + "</span>" +
@@ -734,6 +763,76 @@
734
763
  }).join("");
735
764
  }
736
765
 
766
+ function renderControllerRuns(runs) {
767
+ const container = $("#controller-runs");
768
+ if (!container) return;
769
+
770
+ const recentRuns = (runs || [])
771
+ .slice()
772
+ .sort(function (left, right) { return (right.updatedAt || 0) - (left.updatedAt || 0); })
773
+ .slice(0, 12);
774
+
775
+ if (recentRuns.length === 0) {
776
+ container.innerHTML = '<div class="empty-state">No controller activity yet</div>';
777
+ return;
778
+ }
779
+
780
+ container.innerHTML = recentRuns.map(function (run) {
781
+ const execution = run.execution || {};
782
+ const events = Array.isArray(execution.events) ? execution.events.slice(-5) : [];
783
+ const status = run.status || execution.status || "pending";
784
+ const source = run.source === "task_follow_up"
785
+ ? (run.sourceTaskTitle ? "Follow-up after " + run.sourceTaskTitle : "Workflow follow-up")
786
+ : "Human intake";
787
+ const createdTasks = Array.isArray(run.createdTaskIds) ? run.createdTaskIds : [];
788
+ const createdTaskButtons = createdTasks.length > 0
789
+ ? '<div class="controller-run-created-tasks">' + createdTasks.map(function (taskId) {
790
+ return '<button type="button" class="controller-run-task-link" data-open-task-id="' + escapeHtml(taskId) + '">' + escapeHtml(taskId) + "</button>";
791
+ }).join("") + "</div>"
792
+ : "";
793
+ const replyBlock = run.reply
794
+ ? '<div class="controller-run-section"><div class="controller-run-section-title">Reply</div><div class="markdown-body">' + renderMarkdownContent(run.reply) + "</div></div>"
795
+ : "";
796
+ const errorBlock = run.error
797
+ ? '<div class="controller-run-section"><div class="controller-run-section-title">Error</div><div class="markdown-body">' + renderMarkdownContent(run.error) + "</div></div>"
798
+ : "";
799
+ const eventsBlock = events.length > 0
800
+ ? '<div class="controller-run-events">' + events.map(function (event) {
801
+ const meta = [event.source || "", formatTime(event.createdAt)].filter(Boolean).join(" • ");
802
+ return (
803
+ '<div class="controller-run-event">' +
804
+ ' <div class="controller-run-event-header">' +
805
+ ' <span class="controller-run-event-label">' + escapeHtml(humanizeStatus(event.phase || event.type || "event")) + '</span>' +
806
+ ' <span class="controller-run-event-meta">' + escapeHtml(meta) + "</span>" +
807
+ " </div>" +
808
+ ' <div class="controller-run-event-body markdown-body">' + renderMarkdownContent(event.message || "") + "</div>" +
809
+ "</div>"
810
+ );
811
+ }).join("") + "</div>"
812
+ : "";
813
+
814
+ return (
815
+ '<article class="controller-run-card">' +
816
+ ' <div class="controller-run-header">' +
817
+ ' <div class="controller-run-heading">' +
818
+ ' <div class="controller-run-kicker">' + escapeHtml(source) + "</div>" +
819
+ ' <h3>' + escapeHtml(run.title || "Controller run") + "</h3>" +
820
+ ' <div class="controller-run-meta">Session: ' + escapeHtml(run.sessionKey || "—") + ' • Updated: ' + escapeHtml(formatTime(run.updatedAt) || "—") + "</div>" +
821
+ " </div>" +
822
+ ' <span class="controller-run-status ' + escapeHtml(status) + '">' + escapeHtml(humanizeStatus(status)) + "</span>" +
823
+ " </div>" +
824
+ ' <div class="controller-run-section"><div class="controller-run-section-title">Request</div><div class="markdown-body">' + renderMarkdownContent(run.request || "") + "</div></div>" +
825
+ replyBlock +
826
+ errorBlock +
827
+ (createdTaskButtons
828
+ ? '<div class="controller-run-section"><div class="controller-run-section-title">Created Tasks</div>' + createdTaskButtons + "</div>"
829
+ : "") +
830
+ eventsBlock +
831
+ "</article>"
832
+ );
833
+ }).join("");
834
+ }
835
+
737
836
  function syncSelectedTaskSummary() {
738
837
  if (!selectedTaskId || !selectedTaskDetail) {
739
838
  return;
@@ -873,6 +972,9 @@
873
972
  " <h3>Description</h3>" +
874
973
  renderMarkdownCard(task.description || "No description") +
875
974
  "</div>" +
975
+ (normalizeSkillList(task.recommendedSkills).length > 0
976
+ ? '<div class="task-detail-section"><h3>Recommended Skills</h3>' + renderSkillPills(task.recommendedSkills, "skill-pills task-detail-skill-pills") + "</div>"
977
+ : "") +
876
978
  (task.progress
877
979
  ? '<div class="task-detail-section"><h3>Latest Progress</h3>' + renderMarkdownCard(task.progress) + "</div>"
878
980
  : "") +
@@ -1028,6 +1130,22 @@
1028
1130
  renderTaskDetail();
1029
1131
  }
1030
1132
 
1133
+ function handleControllerRunEvent(payload) {
1134
+ if (!payload || !payload.id) {
1135
+ return;
1136
+ }
1137
+
1138
+ const runs = (teamState.controllerRuns || []).slice();
1139
+ const index = runs.findIndex(function (run) { return run.id === payload.id; });
1140
+ if (index === -1) {
1141
+ runs.push(payload);
1142
+ } else {
1143
+ runs[index] = Object.assign({}, runs[index], payload);
1144
+ }
1145
+ teamState.controllerRuns = runs;
1146
+ renderControllerRuns(teamState.controllerRuns);
1147
+ }
1148
+
1031
1149
  function renderClarifications(clarifications) {
1032
1150
  const container = $("#clarifications-list");
1033
1151
  if (!container) return;
@@ -1255,6 +1373,9 @@
1255
1373
  event.preventDefault();
1256
1374
  const title = $("#task-title").value.trim();
1257
1375
  const desc = $("#task-desc").value.trim();
1376
+ const recommendedSkills = normalizeSkillList(
1377
+ ($("#task-recommended-skills").value || "").split(","),
1378
+ );
1258
1379
  const priority = $("#task-priority").value;
1259
1380
  const role = $("#task-role").value;
1260
1381
 
@@ -1265,6 +1386,9 @@
1265
1386
  if (role) {
1266
1387
  body.assignedRole = role;
1267
1388
  }
1389
+ if (recommendedSkills.length > 0) {
1390
+ body.recommendedSkills = recommendedSkills;
1391
+ }
1268
1392
 
1269
1393
  await apiPost("/tasks", body);
1270
1394
  taskForm.reset();
@@ -1313,6 +1437,18 @@
1313
1437
  }
1314
1438
  });
1315
1439
 
1440
+ const controllerRunsContainer = $("#controller-runs");
1441
+ if (controllerRunsContainer) {
1442
+ controllerRunsContainer.addEventListener("click", function (event) {
1443
+ const target = event.target instanceof Element ? event.target : null;
1444
+ const button = target ? target.closest("[data-open-task-id]") : null;
1445
+ const taskId = button && button.dataset ? button.dataset.openTaskId : "";
1446
+ if (taskId) {
1447
+ openTaskDetail(taskId);
1448
+ }
1449
+ });
1450
+ }
1451
+
1316
1452
  const cmdInput = $("#command-input");
1317
1453
  const cmdSend = $("#command-send");
1318
1454
 
package/src/ui/index.html CHANGED
@@ -105,6 +105,12 @@
105
105
 
106
106
  <!-- Messages Tab -->
107
107
  <div id="tab-messages" class="tab-panel">
108
+ <div class="panel-note">
109
+ Controller activity is persisted here so you can follow requirement intake, orchestration, and follow-up runs from the web UI.
110
+ </div>
111
+ <div id="controller-runs" class="controller-runs">
112
+ <div class="empty-state">No controller activity yet</div>
113
+ </div>
108
114
  <div id="messages-feed" class="messages-feed">
109
115
  <div class="empty-state">No messages yet</div>
110
116
  </div>
@@ -124,6 +130,10 @@
124
130
  <label for="task-desc">Description</label>
125
131
  <textarea id="task-desc" placeholder="Execution-ready task description..." rows="4" required></textarea>
126
132
  </div>
133
+ <div class="form-group">
134
+ <label for="task-recommended-skills">Recommended Skills</label>
135
+ <input type="text" id="task-recommended-skills" placeholder="Comma-separated skill slugs, e.g. find-skills, ui-ux-pro-max">
136
+ </div>
127
137
  <div class="form-row">
128
138
  <div class="form-group">
129
139
  <label for="task-priority">Priority</label>
package/src/ui/style.css CHANGED
@@ -275,14 +275,54 @@ body {
275
275
  .task-priority.critical { background: var(--danger); color: #fff; }
276
276
 
277
277
  .task-body { flex: 1; min-width: 0; }
278
+ .task-title-row {
279
+ display: flex;
280
+ align-items: center;
281
+ justify-content: space-between;
282
+ gap: 10px;
283
+ margin-bottom: 4px;
284
+ }
278
285
  .task-title { font-weight: 600; font-size: 14px; margin-bottom: 4px; }
279
286
  .task-desc { font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; }
287
+ .task-origin-badge {
288
+ font-size: 10px;
289
+ font-weight: 700;
290
+ text-transform: uppercase;
291
+ color: var(--accent);
292
+ background: rgba(96, 165, 250, 0.12);
293
+ border: 1px solid rgba(96, 165, 250, 0.25);
294
+ border-radius: 999px;
295
+ padding: 3px 8px;
296
+ flex-shrink: 0;
297
+ }
280
298
  .task-note {
281
299
  font-size: 12px;
282
300
  color: var(--warning);
283
301
  margin-bottom: 6px;
284
302
  line-height: 1.5;
285
303
  }
304
+ .skill-pills {
305
+ display: flex;
306
+ flex-wrap: wrap;
307
+ gap: 6px;
308
+ }
309
+ .task-skill-pills {
310
+ margin-bottom: 8px;
311
+ }
312
+ .task-detail-skill-pills {
313
+ margin-top: 8px;
314
+ }
315
+ .skill-pill {
316
+ display: inline-flex;
317
+ align-items: center;
318
+ padding: 3px 8px;
319
+ border-radius: 999px;
320
+ background: rgba(52, 211, 153, 0.12);
321
+ border: 1px solid rgba(52, 211, 153, 0.25);
322
+ color: #bbf7d0;
323
+ font-size: 11px;
324
+ font-weight: 600;
325
+ }
286
326
  .task-meta { display: flex; gap: 12px; font-size: 11px; color: var(--text-muted); }
287
327
 
288
328
  .task-status-badge {
@@ -410,6 +450,114 @@ body {
410
450
  justify-content: flex-end;
411
451
  }
412
452
 
453
+ .controller-runs {
454
+ display: flex;
455
+ flex-direction: column;
456
+ gap: 12px;
457
+ margin-bottom: 16px;
458
+ }
459
+
460
+ .controller-run-card {
461
+ background: var(--bg-secondary);
462
+ border: 1px solid var(--border);
463
+ border-radius: var(--radius);
464
+ padding: 14px 16px;
465
+ display: flex;
466
+ flex-direction: column;
467
+ gap: 12px;
468
+ }
469
+
470
+ .controller-run-header {
471
+ display: flex;
472
+ align-items: flex-start;
473
+ justify-content: space-between;
474
+ gap: 16px;
475
+ }
476
+
477
+ .controller-run-heading h3 {
478
+ margin: 0;
479
+ font-size: 15px;
480
+ }
481
+
482
+ .controller-run-kicker,
483
+ .controller-run-meta,
484
+ .controller-run-section-title,
485
+ .controller-run-event-meta {
486
+ color: var(--text-muted);
487
+ font-size: 12px;
488
+ }
489
+
490
+ .controller-run-section {
491
+ display: flex;
492
+ flex-direction: column;
493
+ gap: 6px;
494
+ }
495
+
496
+ .controller-run-status {
497
+ font-size: 11px;
498
+ font-weight: 700;
499
+ text-transform: uppercase;
500
+ padding: 4px 9px;
501
+ border-radius: 999px;
502
+ }
503
+
504
+ .controller-run-status.pending { background: var(--bg-tertiary); color: var(--text-secondary); }
505
+ .controller-run-status.running { background: #422006; color: var(--warning); }
506
+ .controller-run-status.completed { background: #14532d; color: var(--success); }
507
+ .controller-run-status.failed { background: #450a0a; color: var(--danger); }
508
+
509
+ .controller-run-created-tasks {
510
+ display: flex;
511
+ flex-wrap: wrap;
512
+ gap: 8px;
513
+ }
514
+
515
+ .controller-run-task-link {
516
+ padding: 6px 10px;
517
+ border-radius: 999px;
518
+ border: 1px solid rgba(96, 165, 250, 0.25);
519
+ background: rgba(96, 165, 250, 0.12);
520
+ color: var(--accent);
521
+ cursor: pointer;
522
+ font-size: 12px;
523
+ font-weight: 600;
524
+ }
525
+
526
+ .controller-run-task-link:hover {
527
+ border-color: var(--accent);
528
+ }
529
+
530
+ .controller-run-events {
531
+ display: flex;
532
+ flex-direction: column;
533
+ gap: 8px;
534
+ }
535
+
536
+ .controller-run-event {
537
+ border-left: 3px solid var(--accent);
538
+ background: var(--bg-tertiary);
539
+ border-radius: 10px;
540
+ padding: 10px 12px;
541
+ }
542
+
543
+ .controller-run-event-header {
544
+ display: flex;
545
+ align-items: center;
546
+ justify-content: space-between;
547
+ gap: 12px;
548
+ margin-bottom: 6px;
549
+ }
550
+
551
+ .controller-run-event-label {
552
+ font-size: 12px;
553
+ font-weight: 700;
554
+ color: var(--text-primary);
555
+ }
556
+
557
+ .controller-run-event-body {
558
+ font-size: 13px;
559
+ }
560
+
413
561
  .messages-feed {
414
562
  display: flex;
415
563
  flex-direction: column;
@@ -59,6 +59,9 @@ export function createWorkerHttpHandler(
59
59
  const taskId = typeof body.taskId === "string" ? body.taskId : "";
60
60
  const title = typeof body.title === "string" ? body.title : "";
61
61
  const description = typeof body.description === "string" ? body.description : "";
62
+ const recommendedSkills = Array.isArray(body.recommendedSkills)
63
+ ? body.recommendedSkills.map((entry) => String(entry ?? ""))
64
+ : undefined;
62
65
  const repo = body.repo && typeof body.repo === "object"
63
66
  ? body.repo as TaskAssignmentPayload["repo"]
64
67
  : undefined;
@@ -75,6 +78,7 @@ export function createWorkerHttpHandler(
75
78
  taskId,
76
79
  title,
77
80
  description,
81
+ recommendedSkills,
78
82
  repo,
79
83
  })
80
84
  .then((result) => {
@@ -50,6 +50,7 @@ export function createWorkerPromptInjector(
50
50
  parts.push(`10. Valid TeamClaw role IDs: ${TEAMCLAW_ROLE_IDS_TEXT}.`);
51
51
  parts.push("11. Treat file paths from documents, plans, and teammate messages as hints, not guarantees. Verify the real path exists in the current workspace before reading or editing it; if it does not exist, search for the closest real file and note the drift instead of repeatedly calling missing paths.");
52
52
  parts.push("12. The workspace may be backed by a TeamClaw-managed git repository. Treat the current checkout as canonical project state; do not delete `.git` or replace the repo with ad-hoc archives.");
53
+ parts.push("13. If the assigned task includes recommended skills, use those exact skill slugs first. Missing skills should be searched/installed before execution when supported by the runtime.");
53
54
  parts.push(`Worker ID: ${identity.workerId}`);
54
55
  parts.push(`Controller: ${identity.controllerUrl}`);
55
56