fix(ui): roles
This commit is contained in:
parent
7678efedc8
commit
36025c6c67
|
|
@ -70,6 +70,13 @@ class ProvisionOptions:
|
||||||
|
|
||||||
_ROLE_SOUL_MAX_CHARS = 24_000
|
_ROLE_SOUL_MAX_CHARS = 24_000
|
||||||
_ROLE_SOUL_WORD_RE = re.compile(r"[a-z0-9]+")
|
_ROLE_SOUL_WORD_RE = re.compile(r"[a-z0-9]+")
|
||||||
|
_ROLE_INSTRUCTION_START = "<!-- mission-control:role-instructions:start -->"
|
||||||
|
_ROLE_INSTRUCTION_END = "<!-- mission-control:role-instructions:end -->"
|
||||||
|
_ROLE_INSTRUCTION_RE = re.compile(
|
||||||
|
rf"{re.escape(_ROLE_INSTRUCTION_START)}.*?{re.escape(_ROLE_INSTRUCTION_END)}",
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
_ROLE_INSTRUCTION_PATCH_FILES = frozenset({"IDENTITY.md", "SOUL.md"})
|
||||||
|
|
||||||
|
|
||||||
def _is_missing_session_error(exc: OpenClawGatewayError) -> bool:
|
def _is_missing_session_error(exc: OpenClawGatewayError) -> bool:
|
||||||
|
|
@ -290,11 +297,115 @@ def _identity_context(agent: Agent) -> dict[str, str]:
|
||||||
return {**identity_context, **extra_identity_context}
|
return {**identity_context, **extra_identity_context}
|
||||||
|
|
||||||
|
|
||||||
|
def _role_instruction_context(agent: Agent) -> dict[str, str]:
|
||||||
|
role = _identity_context(agent).get("identity_role", "")
|
||||||
|
return {
|
||||||
|
"local_role_instruction_markdown": _local_role_instruction_markdown(role),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _role_slug(role: str) -> str:
|
def _role_slug(role: str) -> str:
|
||||||
tokens = _ROLE_SOUL_WORD_RE.findall(role.strip().lower())
|
tokens = _ROLE_SOUL_WORD_RE.findall(role.strip().lower())
|
||||||
return "-".join(tokens)
|
return "-".join(tokens)
|
||||||
|
|
||||||
|
|
||||||
|
_ROLE_INSTRUCTION_MARKDOWN_BY_SLUG: dict[str, str] = {
|
||||||
|
"backend-code": """## Role Instructions - Backend Code
|
||||||
|
|
||||||
|
- Own server-side behavior: APIs, services, data models, migrations, background jobs, integrations, and backend tests.
|
||||||
|
- Start by reading the relevant backend route, schema, model, service, and migration paths before editing.
|
||||||
|
- Preserve API contracts unless the task explicitly asks for a contract change; when a contract changes, update callers and generated clients as needed.
|
||||||
|
- Add or update focused backend tests for changed behavior, including permission, validation, and persistence edge cases.
|
||||||
|
- Call out risks around data integrity, backwards compatibility, idempotency, concurrency, and operational rollout in task comments.""",
|
||||||
|
"ui": """## Role Instructions - UI
|
||||||
|
|
||||||
|
- Own the user-facing experience: page structure, component behavior, responsive layout, accessibility, and interaction polish.
|
||||||
|
- Follow the existing design system and component patterns before adding new UI abstractions.
|
||||||
|
- Check empty, loading, error, disabled, long-text, and mobile states for every changed workflow.
|
||||||
|
- Keep copy clear and action-oriented; avoid explanatory clutter inside the app.
|
||||||
|
- Verify visual changes with the strongest local signal available, such as lint, tests, builds, or screenshots when a browser workflow is practical.""",
|
||||||
|
"security-review": """## Role Instructions - Security Review
|
||||||
|
|
||||||
|
- Review for concrete security risks before implementation convenience: auth boundaries, authorization, secrets, injection, unsafe redirects, SSRF, XSS, CSRF, file access, and auditability.
|
||||||
|
- Treat trust boundaries explicitly. Identify who can call the path, what data they can reach, and what external side effects can happen.
|
||||||
|
- Prefer minimal, targeted fixes that reduce exploitability without changing unrelated behavior.
|
||||||
|
- Include regression tests or clear verification evidence for the security boundary being protected.
|
||||||
|
- Report residual risk plainly, including assumptions and any follow-up hardening that should be tracked separately.""",
|
||||||
|
"code-review": """## Role Instructions - Code Review
|
||||||
|
|
||||||
|
- Review before rewriting. Prioritize correctness, regressions, missing tests, maintainability, and operational risk.
|
||||||
|
- Lead with specific findings tied to files, lines, payloads, or workflows; keep summaries secondary.
|
||||||
|
- Distinguish must-fix defects from style preferences, and avoid broad refactors unless they directly reduce task risk.
|
||||||
|
- Verify claims against the codebase and tests when possible.
|
||||||
|
- If asked to implement review fixes, keep edits scoped to the identified issues and preserve unrelated user work.""",
|
||||||
|
"qa": """## Role Instructions - QA
|
||||||
|
|
||||||
|
- Own verification strategy: expected behavior, edge cases, regression paths, and evidence quality.
|
||||||
|
- Convert requirements into concrete test cases before executing or recommending changes.
|
||||||
|
- Exercise happy paths, boundary cases, permissions, failure states, and cross-browser or responsive concerns when relevant.
|
||||||
|
- Record reproducible steps for failures and include the smallest useful evidence in task comments.
|
||||||
|
- Prefer automated regression coverage for behavior that is likely to recur.""",
|
||||||
|
"devops": """## Role Instructions - DevOps
|
||||||
|
|
||||||
|
- Own deployment, runtime configuration, observability, infrastructure, CI/CD, and operational reliability concerns.
|
||||||
|
- Check environment variables, secrets handling, logs, health checks, migrations, rollbacks, and resource limits.
|
||||||
|
- Prefer idempotent, reversible operational changes with clear verification commands.
|
||||||
|
- Avoid destructive infrastructure or production-impacting actions without explicit approval.
|
||||||
|
- Document rollout risks, monitoring signals, and rollback paths when task changes affect operations.""",
|
||||||
|
"data": """## Role Instructions - Data
|
||||||
|
|
||||||
|
- Own data shape, quality, querying, transformations, analytics, and persistence semantics.
|
||||||
|
- Validate source assumptions, null behavior, units, time zones, deduplication, and migration/backfill needs.
|
||||||
|
- Prefer structured parsers and typed schemas over ad hoc string handling.
|
||||||
|
- Add focused checks for data integrity and representative edge cases.
|
||||||
|
- Explain any data caveats, sampling limits, or confidence constraints in task comments.""",
|
||||||
|
"documentation": """## Role Instructions - Documentation
|
||||||
|
|
||||||
|
- Own clarity, accuracy, structure, and maintenance burden of written artifacts.
|
||||||
|
- Verify commands, paths, API names, and workflow steps against the repository before documenting them.
|
||||||
|
- Write for the target reader's next action, not for exhaustive narration.
|
||||||
|
- Keep docs consistent with existing terminology and formatting.
|
||||||
|
- Flag stale or conflicting documentation discovered while working.""",
|
||||||
|
"product": """## Role Instructions - Product
|
||||||
|
|
||||||
|
- Own user outcome, scope clarity, acceptance criteria, and tradeoff framing.
|
||||||
|
- Translate ambiguity into concrete workflow, success metric, and done-signal questions or proposals.
|
||||||
|
- Prioritize the smallest useful increment that preserves the intended user experience.
|
||||||
|
- Consider permissions, onboarding, empty states, and operational support as part of the product surface.
|
||||||
|
- Capture decisions and open questions in task comments so implementation stays aligned.""",
|
||||||
|
"triage": """## Role Instructions - Triage
|
||||||
|
|
||||||
|
- Own intake clarity, prioritization, routing, and next-action definition.
|
||||||
|
- Reproduce or validate the issue when possible, then classify severity, owner, affected surface, and likely cause.
|
||||||
|
- Split vague work into actionable tasks with clear acceptance criteria and dependencies.
|
||||||
|
- Route work to the best-fit agent role and explain why when the assignment is not obvious.
|
||||||
|
- Keep comments concise: current state, blocker if any, next action, and owner.""",
|
||||||
|
"generalist": """## Role Instructions - Generalist
|
||||||
|
|
||||||
|
- Adapt to the task context while staying inside the repository's existing architecture and conventions.
|
||||||
|
- Read enough surrounding code to avoid local fixes that break adjacent workflows.
|
||||||
|
- Prefer focused changes with direct verification evidence.
|
||||||
|
- Escalate when the task needs a specialist lens such as security, data, UI, or backend architecture.
|
||||||
|
- Leave task comments that make the outcome and remaining risk easy for the lead to understand.""",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _local_role_instruction_markdown(role: str) -> str:
|
||||||
|
role = role.strip()
|
||||||
|
if not role:
|
||||||
|
return ""
|
||||||
|
markdown = _ROLE_INSTRUCTION_MARKDOWN_BY_SLUG.get(_role_slug(role))
|
||||||
|
if markdown:
|
||||||
|
return markdown
|
||||||
|
return f"""## Role Instructions - {role}
|
||||||
|
|
||||||
|
- Treat `{role}` as your execution lens for assigned tasks.
|
||||||
|
- Start each task by identifying the artifact you own, the quality bar for that role, and the evidence needed to prove completion.
|
||||||
|
- Stay within the role unless the task or lead explicitly asks you to broaden scope.
|
||||||
|
- When work crosses into another specialty, call that out and suggest the best-fit collaborator or review path.
|
||||||
|
- Capture role-specific assumptions, risks, and verification evidence in task comments."""
|
||||||
|
|
||||||
|
|
||||||
def _select_role_soul_ref(
|
def _select_role_soul_ref(
|
||||||
refs: list[souls_directory.SoulRef],
|
refs: list[souls_directory.SoulRef],
|
||||||
*,
|
*,
|
||||||
|
|
@ -373,6 +484,7 @@ def _build_context(
|
||||||
base_url = settings.base_url
|
base_url = settings.base_url
|
||||||
main_session_key = GatewayAgentIdentity.session_key(gateway)
|
main_session_key = GatewayAgentIdentity.session_key(gateway)
|
||||||
identity_context = _identity_context(agent)
|
identity_context = _identity_context(agent)
|
||||||
|
role_instruction_context = _role_instruction_context(agent)
|
||||||
user_context = _user_context(user)
|
user_context = _user_context(user)
|
||||||
return {
|
return {
|
||||||
"agent_name": agent.name,
|
"agent_name": agent.name,
|
||||||
|
|
@ -402,6 +514,7 @@ def _build_context(
|
||||||
"workspace_root": workspace_root,
|
"workspace_root": workspace_root,
|
||||||
**user_context,
|
**user_context,
|
||||||
**identity_context,
|
**identity_context,
|
||||||
|
**role_instruction_context,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -413,6 +526,7 @@ def _build_main_context(
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
base_url = settings.base_url
|
base_url = settings.base_url
|
||||||
identity_context = _identity_context(agent)
|
identity_context = _identity_context(agent)
|
||||||
|
role_instruction_context = _role_instruction_context(agent)
|
||||||
user_context = _user_context(user)
|
user_context = _user_context(user)
|
||||||
return {
|
return {
|
||||||
"agent_name": agent.name,
|
"agent_name": agent.name,
|
||||||
|
|
@ -425,6 +539,7 @@ def _build_main_context(
|
||||||
"workspace_root": gateway.workspace_root or "",
|
"workspace_root": gateway.workspace_root or "",
|
||||||
**user_context,
|
**user_context,
|
||||||
**identity_context,
|
**identity_context,
|
||||||
|
**role_instruction_context,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@
|
||||||
- Communication Style: {{ identity_communication_style }}
|
- Communication Style: {{ identity_communication_style }}
|
||||||
- Emoji: {{ identity_emoji }}
|
- Emoji: {{ identity_emoji }}
|
||||||
|
|
||||||
|
{% set local_role_instructions = local_role_instruction_markdown | default("") | trim %}
|
||||||
|
{% if local_role_instructions %}
|
||||||
|
{{ local_role_instructions }}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
{% if identity_purpose or is_lead %}
|
{% if identity_purpose or is_lead %}
|
||||||
## Purpose
|
## Purpose
|
||||||
{% if identity_purpose %}
|
{% if identity_purpose %}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
{% set is_lead = (is_board_lead | default(false) | string | lower) in ["true", "1", "yes"] %}
|
{% set is_lead = (is_board_lead | default(false) | string | lower) in ["true", "1", "yes"] %}
|
||||||
|
{% set local_role_instructions = local_role_instruction_markdown | default("") | trim %}
|
||||||
{% if is_lead %}
|
{% if is_lead %}
|
||||||
# SOUL.md - Who You Are
|
# SOUL.md - Who You Are
|
||||||
|
|
||||||
|
|
@ -24,12 +25,22 @@ Be the assistant you’d actually want to talk to. Concise when needed, thorough
|
||||||
Each session starts fresh. `MEMORY.md` and `USER.md` are your continuity anchors.
|
Each session starts fresh. `MEMORY.md` and `USER.md` are your continuity anchors.
|
||||||
If this file changes materially, make that explicit in your next status update.
|
If this file changes materially, make that explicit in your next status update.
|
||||||
|
|
||||||
|
{% if local_role_instructions %}
|
||||||
|
{{ local_role_instructions }}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
This file is yours to evolve. As you learn who you are, update it.
|
This file is yours to evolve. As you learn who you are, update it.
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set remote_role_soul = directory_role_soul_markdown | default("") | trim %}
|
{% set remote_role_soul = directory_role_soul_markdown | default("") | trim %}
|
||||||
{% if remote_role_soul %}
|
{% if remote_role_soul %}
|
||||||
{{ remote_role_soul }}
|
{{ remote_role_soul }}
|
||||||
|
{% if local_role_instructions %}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
{{ local_role_instructions }}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
# SOUL.md
|
# SOUL.md
|
||||||
|
|
||||||
|
|
@ -38,6 +49,10 @@ _You're not a chatbot. You're becoming someone._
|
||||||
This file is your stable core. Changes here should be rare and significant.
|
This file is your stable core. Changes here should be rare and significant.
|
||||||
Put evolving preferences and identity changes in `MEMORY.md`.
|
Put evolving preferences and identity changes in `MEMORY.md`.
|
||||||
|
|
||||||
|
{% if local_role_instructions %}
|
||||||
|
{{ local_role_instructions }}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
## Core Truths
|
## Core Truths
|
||||||
|
|
||||||
**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" -- just help. Actions speak louder than filler words.
|
**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" -- just help. Actions speak louder than filler words.
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,7 @@ This avoids relying on startup hooks to populate `api/openapi.json`.
|
||||||
- `identity_role`, `identity_communication_style`, `identity_emoji`
|
- `identity_role`, `identity_communication_style`, `identity_emoji`
|
||||||
- `identity_autonomy_level`, `identity_verbosity`, `identity_output_format`, `identity_update_cadence`
|
- `identity_autonomy_level`, `identity_verbosity`, `identity_output_format`, `identity_update_cadence`
|
||||||
- `identity_purpose`, `identity_personality`, `identity_custom_instructions`
|
- `identity_purpose`, `identity_personality`, `identity_custom_instructions`
|
||||||
|
- `local_role_instruction_markdown`: deterministic local instructions derived from `identity_role`
|
||||||
|
|
||||||
### Board-agent-only keys
|
### Board-agent-only keys
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,60 @@ def test_user_context_prefers_name_token_when_preferred_name_missing():
|
||||||
assert context["user_preferred_name"] == "Jane"
|
assert context["user_preferred_name"] == "Jane"
|
||||||
|
|
||||||
|
|
||||||
|
def test_local_role_instruction_catalog_includes_backend_code() -> None:
|
||||||
|
markdown = agent_provisioning._local_role_instruction_markdown("Backend Code")
|
||||||
|
|
||||||
|
assert "## Role Instructions - Backend Code" in markdown
|
||||||
|
assert "APIs, services, data models, migrations" in markdown
|
||||||
|
assert "backend tests" in markdown
|
||||||
|
|
||||||
|
|
||||||
|
def test_local_role_instruction_has_custom_role_fallback() -> None:
|
||||||
|
markdown = agent_provisioning._local_role_instruction_markdown("Release Captain")
|
||||||
|
|
||||||
|
assert "## Role Instructions - Release Captain" in markdown
|
||||||
|
assert "execution lens" in markdown
|
||||||
|
assert "best-fit collaborator" in markdown
|
||||||
|
|
||||||
|
|
||||||
|
def test_rendered_agent_files_include_local_role_instructions() -> None:
|
||||||
|
agent = _AgentStub(
|
||||||
|
name="Backend Agent",
|
||||||
|
identity_profile={"role": "Backend Code"},
|
||||||
|
)
|
||||||
|
context = {
|
||||||
|
"agent_name": agent.name,
|
||||||
|
"agent_id": str(agent.id),
|
||||||
|
"identity_role": "Backend Code",
|
||||||
|
"identity_communication_style": "direct",
|
||||||
|
"identity_emoji": ":gear:",
|
||||||
|
"identity_purpose": "",
|
||||||
|
"identity_personality": "",
|
||||||
|
"identity_custom_instructions": "",
|
||||||
|
"is_board_lead": "false",
|
||||||
|
"directory_role_soul_markdown": "",
|
||||||
|
"directory_role_soul_source_url": "",
|
||||||
|
"local_role_instruction_markdown": agent_provisioning._local_role_instruction_markdown(
|
||||||
|
"Backend Code"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered = agent_provisioning._render_agent_files(
|
||||||
|
context,
|
||||||
|
agent, # type: ignore[arg-type]
|
||||||
|
{"IDENTITY.md", "SOUL.md"},
|
||||||
|
include_bootstrap=False,
|
||||||
|
template_overrides={
|
||||||
|
"IDENTITY.md": "BOARD_IDENTITY.md.j2",
|
||||||
|
"SOUL.md": "BOARD_SOUL.md.j2",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "## Role Instructions - Backend Code" in rendered["IDENTITY.md"]
|
||||||
|
assert "## Role Instructions - Backend Code" in rendered["SOUL.md"]
|
||||||
|
assert "backend tests" in rendered["SOUL.md"]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _GatewayStub:
|
class _GatewayStub:
|
||||||
id: UUID
|
id: UUID
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
useListBoardsApiV1BoardsGet,
|
useListBoardsApiV1BoardsGet,
|
||||||
} from "@/api/generated/boards/boards";
|
} from "@/api/generated/boards/boards";
|
||||||
import type { AgentRead, AgentUpdate, BoardRead } from "@/api/generated/model";
|
import type { AgentRead, AgentUpdate, BoardRead } from "@/api/generated/model";
|
||||||
|
import { AgentRoleField } from "@/components/agents/AgentRoleField";
|
||||||
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
|
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
@ -277,23 +278,18 @@ export default function EditAgentPage() {
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<AgentRoleField
|
||||||
<label className="text-sm font-medium text-foreground">
|
|
||||||
Role
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={resolvedIdentityProfile.role}
|
value={resolvedIdentityProfile.role}
|
||||||
onChange={(event) =>
|
onChange={(role) =>
|
||||||
setIdentityProfile({
|
setIdentityProfile({
|
||||||
...resolvedIdentityProfile,
|
...resolvedIdentityProfile,
|
||||||
role: event.target.value,
|
role,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder="e.g. Founder, Social Media Manager"
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
listId="edit-agent-role-options"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import {
|
||||||
formatRelativeTimestamp as formatRelative,
|
formatRelativeTimestamp as formatRelative,
|
||||||
formatTimestamp,
|
formatTimestamp,
|
||||||
} from "@/lib/formatters";
|
} from "@/lib/formatters";
|
||||||
|
import { getAgentRole } from "@/lib/agent-roles";
|
||||||
import { useOrganizationMembership } from "@/lib/use-organization-membership";
|
import { useOrganizationMembership } from "@/lib/use-organization-membership";
|
||||||
import type {
|
import type {
|
||||||
ActivityEventRead,
|
ActivityEventRead,
|
||||||
|
|
@ -138,6 +139,7 @@ export default function AgentDetailPage() {
|
||||||
|
|
||||||
const isDeleting = deleteMutation.isPending;
|
const isDeleting = deleteMutation.isPending;
|
||||||
const agentStatus = agent?.status ?? "unknown";
|
const agentStatus = agent?.status ?? "unknown";
|
||||||
|
const agentRole = getAgentRole(agent?.identity_profile);
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (!agentId || !isSignedIn) return;
|
if (!agentId || !isSignedIn) return;
|
||||||
|
|
@ -263,6 +265,14 @@ export default function AgentDetailPage() {
|
||||||
<p className="mt-1 text-sm text-strong">—</p>
|
<p className="mt-1 text-sm text-strong">—</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-quiet">
|
||||||
|
Role
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-strong">
|
||||||
|
{agentRole}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-quiet">
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-quiet">
|
||||||
Last seen
|
Last seen
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
import { useCreateAgentApiV1AgentsPost } from "@/api/generated/agents/agents";
|
import { useCreateAgentApiV1AgentsPost } from "@/api/generated/agents/agents";
|
||||||
import { useOrganizationMembership } from "@/lib/use-organization-membership";
|
import { useOrganizationMembership } from "@/lib/use-organization-membership";
|
||||||
import type { BoardRead } from "@/api/generated/model";
|
import type { BoardRead } from "@/api/generated/model";
|
||||||
|
import { AgentRoleField } from "@/components/agents/AgentRoleField";
|
||||||
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
|
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
@ -161,23 +162,18 @@ export default function NewAgentPage() {
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<AgentRoleField
|
||||||
<label className="text-sm font-medium text-foreground">
|
|
||||||
Role
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
value={identityProfile.role}
|
value={identityProfile.role}
|
||||||
onChange={(event) =>
|
onChange={(role) =>
|
||||||
setIdentityProfile((current) => ({
|
setIdentityProfile((current) => ({
|
||||||
...current,
|
...current,
|
||||||
role: event.target.value,
|
role,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
placeholder="e.g. Founder, Social Media Manager"
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
listId="new-agent-role-options"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-foreground">
|
<label className="text-sm font-medium text-foreground">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
AGENT_ROLE_OPTIONS,
|
||||||
|
PRIMARY_AGENT_ROLE_OPTIONS,
|
||||||
|
} from "@/lib/agent-roles";
|
||||||
|
|
||||||
|
type AgentRoleFieldProps = {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
listId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AgentRoleField({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
listId,
|
||||||
|
}: AgentRoleFieldProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-foreground">Role</label>
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
list={listId}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
placeholder="e.g. Backend Code, UI, Security Review"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<datalist id={listId}>
|
||||||
|
{AGENT_ROLE_OPTIONS.map((role) => (
|
||||||
|
<option key={role} value={role} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{PRIMARY_AGENT_ROLE_OPTIONS.map((role) => (
|
||||||
|
<Button
|
||||||
|
key={role}
|
||||||
|
type="button"
|
||||||
|
variant={value === role ? "primary" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
className="h-8 rounded-lg px-2.5 text-xs"
|
||||||
|
onClick={() => onChange(role)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{role}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ const buildAgent = (overrides: Partial<AgentRead> = {}): AgentRead => ({
|
||||||
board_id: "board-1",
|
board_id: "board-1",
|
||||||
status: "online",
|
status: "online",
|
||||||
openclaw_session_id: "session-1234",
|
openclaw_session_id: "session-1234",
|
||||||
|
identity_profile: { role: "Backend Code" },
|
||||||
last_seen_at: "2026-01-01T00:00:00Z",
|
last_seen_at: "2026-01-01T00:00:00Z",
|
||||||
created_at: "2026-01-01T00:00:00Z",
|
created_at: "2026-01-01T00:00:00Z",
|
||||||
updated_at: "2026-01-01T00:00:00Z",
|
updated_at: "2026-01-01T00:00:00Z",
|
||||||
|
|
@ -61,6 +62,7 @@ describe("AgentsTable", () => {
|
||||||
"href",
|
"href",
|
||||||
"/boards/board-1",
|
"/boards/board-1",
|
||||||
);
|
);
|
||||||
|
expect(screen.getByRole("cell", { name: "Backend Code" })).toBeVisible();
|
||||||
expect(screen.getByRole("link", { name: "Edit" })).toHaveAttribute(
|
expect(screen.getByRole("link", { name: "Edit" })).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
"/agents/agent-1/edit",
|
"/agents/agent-1/edit",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
linkifyCell,
|
linkifyCell,
|
||||||
pillCell,
|
pillCell,
|
||||||
} from "@/components/tables/cell-formatters";
|
} from "@/components/tables/cell-formatters";
|
||||||
|
import { getAgentRole } from "@/lib/agent-roles";
|
||||||
import { truncateText as truncate } from "@/lib/formatters";
|
import { truncateText as truncate } from "@/lib/formatters";
|
||||||
|
|
||||||
type AgentsTableEmptyState = {
|
type AgentsTableEmptyState = {
|
||||||
|
|
@ -116,6 +117,16 @@ export function AgentsTable({
|
||||||
header: "Status",
|
header: "Status",
|
||||||
cell: ({ row }) => pillCell(row.original.status),
|
cell: ({ row }) => pillCell(row.original.status),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "role",
|
||||||
|
header: "Role",
|
||||||
|
accessorFn: (agent) => getAgentRole(agent.identity_profile),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-sm text-foreground">
|
||||||
|
{getAgentRole(row.original.identity_profile)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "openclaw_session_id",
|
accessorKey: "openclaw_session_id",
|
||||||
header: "Session",
|
header: "Session",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
export const AGENT_ROLE_OPTIONS = [
|
||||||
|
"Generalist",
|
||||||
|
"Backend Code",
|
||||||
|
"UI",
|
||||||
|
"Security Review",
|
||||||
|
"Code Review",
|
||||||
|
"QA",
|
||||||
|
"DevOps",
|
||||||
|
"Data",
|
||||||
|
"Documentation",
|
||||||
|
"Product",
|
||||||
|
"Triage",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const PRIMARY_AGENT_ROLE_OPTIONS = [
|
||||||
|
"Backend Code",
|
||||||
|
"UI",
|
||||||
|
"Security Review",
|
||||||
|
"Code Review",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const getAgentRole = (
|
||||||
|
identityProfile: unknown,
|
||||||
|
fallback = "Unassigned",
|
||||||
|
): string => {
|
||||||
|
if (!identityProfile || typeof identityProfile !== "object") {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
const role = (identityProfile as Record<string, unknown>).role;
|
||||||
|
return typeof role === "string" && role.trim() ? role.trim() : fallback;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue