From b373f090bd8a7b35106dd67f4e4f088ab119ff1d Mon Sep 17 00:00:00 2001 From: Michael Lam Date: Fri, 15 May 2026 19:41:02 -0700 Subject: [PATCH] feat: clarify profiles and workspaces --- CHANGELOG.md | 2 + static/panels.js | 38 ++++++++++++++++++- .../test_issue2147_profile_workspace_copy.py | 32 ++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/test_issue2147_profile_workspace_copy.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c86c32d2..f3e2e67e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ ### Added +- **PR #2343** by @Michaelyklam (refs #2147) — The Profiles panel now includes an inline "Profiles vs workspaces" explainer. The copy clarifies that profiles control how the agent works — identity, memory, skills, model/provider config, and tools — while workspaces control what project/files a session operates on, making the OpenClaw-style role/profile mental model easier to map onto Hermes WebUI. + - **PR #2332** by @Michaelyklam (refs #2290) — Cron run history/output cards now surface token/cost metadata when the underlying cron output markdown includes it. The backend parses optional model/token/cost/duration frontmatter from cron output files and returns it from `/api/crons/history` and `/api/crons/run`; the Tasks panel renders a compact usage strip beside run rows and below expanded output without affecting older outputs that lack usage metadata. ### Fixed diff --git a/static/panels.js b/static/panels.js index 0f9ccedb..47d38561 100644 --- a/static/panels.js +++ b/static/panels.js @@ -4465,8 +4465,22 @@ async function loadProfilesPanel() { const data = await api('/api/profiles'); _profilesCache = data; panel.innerHTML = ''; + const explainer = document.createElement('div'); + explainer.className = 'profile-card profile-help-card'; + explainer.innerHTML = ` +
+
+
Profiles vs workspaces
+
Use profiles for how the agent works; use workspaces for what files it works on.
+
+
`; + explainer.onclick = () => _renderProfileConceptHelp(data.active || 'default'); + panel.appendChild(explainer); if (!data.profiles || !data.profiles.length) { - panel.innerHTML = `
${esc(t('profiles_no_profiles'))}
`; + const emptyMsg = document.createElement('div'); + emptyMsg.style.cssText = 'padding:16px;color:var(--muted);font-size:12px'; + emptyMsg.textContent = t('profiles_no_profiles'); + panel.appendChild(emptyMsg); if (_profileMode !== 'create') _clearProfileDetail(); return; } @@ -4509,6 +4523,28 @@ async function loadProfilesPanel() { } } +function _renderProfileConceptHelp(activeName){ + const title = $('profileDetailTitle'); + const body = $('profileDetailBody'); + const empty = $('profileDetailEmpty'); + if (!title || !body) return; + title.textContent = 'Profiles vs workspaces'; + body.innerHTML = ` +
+
+
Use profiles for how; workspaces for what
+
Profiles
Agent identity, memory, skills, model/provider config, and connected tools. Create profiles for roles like researcher, writer, marketer, or developer when those roles should carry different context or capabilities.
+
Workspaces
Project or product folders on disk. Use one workspace per repo/product so chat, terminal, and file browsing point at the right files.
+
Together
A profile can have a default workspace, but you can still switch workspaces for a session. Profiles answer “who is working?”; workspaces answer “where are they working?”
+
+
`; + body.style.display = ''; + if (empty) empty.style.display = 'none'; + _profileMode = 'read'; + _currentProfileDetail = null; + _setProfileHeaderButtons('empty'); +} + function _renderProfileDetail(p, activeName){ _currentProfileDetail = p; const title = $('profileDetailTitle'); diff --git a/tests/test_issue2147_profile_workspace_copy.py b/tests/test_issue2147_profile_workspace_copy.py new file mode 100644 index 00000000..55203c15 --- /dev/null +++ b/tests/test_issue2147_profile_workspace_copy.py @@ -0,0 +1,32 @@ +"""Regression tests for issue #2147 profile/workspace mental-model copy.""" +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent + + +def read(rel: str) -> str: + return (REPO / rel).read_text(encoding="utf-8") + + +def test_profiles_panel_surfaces_profiles_vs_workspaces_help_card(): + src = read("static/panels.js") + assert "Profiles vs workspaces" in src + assert "Use profiles for how the agent works; use workspaces for what files it works on." in src + assert "_renderProfileConceptHelp" in src + assert "explainer.onclick = () => _renderProfileConceptHelp" in src + + +def test_profile_concept_help_distinguishes_how_from_where(): + src = read("static/panels.js") + assert "Agent identity, memory, skills, model/provider config, and connected tools" in src + assert "Create profiles for roles like researcher, writer, marketer, or developer" in src + assert "Project or product folders on disk" in src + assert "Profiles answer “who is working?”; workspaces answer “where are they working?”" in src + + +def test_empty_profiles_state_keeps_help_card_visible(): + src = read("static/panels.js") + assert "panel.innerHTML = ''" in src + assert "panel.appendChild(explainer)" in src + assert "emptyMsg.textContent = t('profiles_no_profiles')" in src + assert "panel.appendChild(emptyMsg)" in src