mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-11 14:50:14 +00:00
Personas: requires_folder/subagents/scheduling traits replace family and the workspace enum
Manifest keeps a legacy family shim; telemetry wire fields unchanged. No behavior change; spec in ocw-context/docs/workspace-scratch-design.md.
This commit is contained in:
@@ -58,13 +58,13 @@ const SETTINGS = {
|
||||
const PERSONAS = {
|
||||
internal: true,
|
||||
personas: [
|
||||
{ id: "cowork", name: "OpenWorker", icon: "cowork", tagline: "Produce a deliverable — research, analysis, scripts", needs_workspace: true, builtin: true, family: "knowledge", workspace: "deliverable", tools: ["files", "search"], enabled: true, surfaced: true, default: true, ships: true, group: "general" },
|
||||
{ id: "code", name: "Code", icon: "code", tagline: "Work in a codebase — files, git, shell", needs_workspace: true, builtin: true, family: "code", workspace: "git", tools: ["code_files", "git"], enabled: false, surfaced: false, default: false, ships: true, group: "general" },
|
||||
{ id: "security", name: "Security Coworker", icon: "shield", tagline: "Find and fix security issues — scan, triage, PR", needs_workspace: true, builtin: true, family: "code", workspace: "git", tools: ["code_files", "git", "shell"], enabled: true, surfaced: true, default: false, ships: true, group: "security" },
|
||||
{ id: "ops", name: "Ops Coworker", icon: "wrench", tagline: "Operate and investigate — runbooks, logs, infrastructure", needs_workspace: true, builtin: true, family: "knowledge", workspace: "deliverable", tools: ["files", "shell"], enabled: true, surfaced: true, default: false, ships: false, group: "general" },
|
||||
{ id: "cowork", name: "OpenWorker", icon: "cowork", tagline: "Produce a deliverable — research, analysis, scripts", requires_folder: false, builtin: true, tools: ["files", "search"], enabled: true, surfaced: true, default: true, ships: true, group: "general" },
|
||||
{ id: "code", name: "Code", icon: "code", tagline: "Work in a codebase — files, git, shell", requires_folder: true, builtin: true, tools: ["code_files", "git"], enabled: false, surfaced: false, default: false, ships: true, group: "general" },
|
||||
{ id: "security", name: "Security Coworker", icon: "shield", tagline: "Find and fix security issues — scan, triage, PR", requires_folder: true, builtin: true, tools: ["code_files", "git", "shell"], enabled: true, surfaced: true, default: false, ships: true, group: "security" },
|
||||
{ id: "ops", name: "Ops Coworker", icon: "wrench", tagline: "Operate and investigate — runbooks, logs, infrastructure", requires_folder: false, builtin: true, tools: ["files", "shell"], enabled: true, surfaced: true, default: false, ships: false, group: "general" },
|
||||
// A non-builtin install (disabled pending consent — invisible to picker specs) so the
|
||||
// Personas page's delete/enable affordances have a target.
|
||||
{ id: "acme-notes", name: "Acme Notes", icon: "pencil", tagline: "Acme's note-taking coworker", needs_workspace: true, builtin: false, family: "knowledge", workspace: "deliverable", tools: ["files"], enabled: false, surfaced: false, default: false, ships: true, group: "general" },
|
||||
{ id: "acme-notes", name: "Acme Notes", icon: "pencil", tagline: "Acme's note-taking coworker", requires_folder: false, builtin: false, tools: ["files"], enabled: false, surfaced: false, default: false, ships: true, group: "general" },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1334,8 +1334,8 @@ export async function mockApi(page: import("@playwright/test").Page) {
|
||||
// Sharing v1: a bundle zip import — consent with version + replaces + recommends.
|
||||
const imported = {
|
||||
id: "team-sec", name: "Team Security Coworker", icon: "shield",
|
||||
tagline: "Our security playbook", needs_workspace: true, builtin: false,
|
||||
family: "code", workspace: "git", tools: ["code_files", "search", "shell"],
|
||||
tagline: "Our security playbook", requires_folder: true, builtin: false,
|
||||
tools: ["code_files", "search", "shell"],
|
||||
enabled: false, surfaced: false, default: false, version: "2",
|
||||
};
|
||||
if (!personas.some((x) => x.id === "team-sec")) personas.push(imported);
|
||||
|
||||
+10
-15
@@ -113,9 +113,8 @@ function normalizeTodos(raw: unknown): TodoItem[] {
|
||||
});
|
||||
}
|
||||
|
||||
// Fallbacks used only before the persona list loads (the in-component, family-aware
|
||||
// needsWorkspace/gatesWorkspace consult the real persona once available).
|
||||
const needsWorkspaceFallback = (a: string) => a === "code" || a === "cowork";
|
||||
// Fallback used only before the persona list loads (the in-component gatesWorkspace
|
||||
// consults the real persona's requires_folder once available).
|
||||
const gatesWorkspaceFallback = (a: string) => a === "code";
|
||||
const LAST_SESSION_KEY = "coworker:last-session-by-agent:v1";
|
||||
const NAV_COLLAPSED_KEY = "coworker:nav-collapsed:v1";
|
||||
@@ -404,11 +403,9 @@ export function App() {
|
||||
getInbox(sessionId, "pending").then(setSessionInbox).catch(() => setSessionInbox([]));
|
||||
refreshSessions(); // attention badge should drop right away
|
||||
};
|
||||
// Shows a working-area chip / project grouping. Persona's needs_workspace; fallback before load.
|
||||
const needsWorkspace = (a: string) => personaOf(a)?.needs_workspace ?? needsWorkspaceFallback(a);
|
||||
// MUST pick a folder before starting — project-scoped personas (git-bound Code, project-bound
|
||||
// Ops). Scratch/deliverable personas start orphan: the server auto-provisions a per-conversation
|
||||
// scratch dir and reports it in the `ready` event.
|
||||
// MUST pick a folder before starting — requires_folder personas (git-bound Code, the
|
||||
// security coworkers). Everything else starts orphan: the server auto-provisions a
|
||||
// per-conversation scratch dir and reports it in the `ready` event.
|
||||
const gatesWorkspace = (a: string) => {
|
||||
const p = personaOf(a);
|
||||
return p ? isProjectScoped(p) : gatesWorkspaceFallback(a);
|
||||
@@ -1343,9 +1340,7 @@ export function App() {
|
||||
// starts orphan ("" → server provisions). Chat has no workspace.
|
||||
const targetWorkspace = gatesWorkspace(name)
|
||||
? target.workspace || fallbackWorkspace(inheritable, knownProjects)
|
||||
: needsWorkspace(name)
|
||||
? target.workspace || ""
|
||||
: "";
|
||||
: target.workspace || "";
|
||||
if (targetWorkspace && targetWorkspace !== workspace) {
|
||||
setWorkspace(targetWorkspace);
|
||||
setBranch(null);
|
||||
@@ -1372,7 +1367,7 @@ export function App() {
|
||||
if (fallback && fallback !== workspace) {
|
||||
setWorkspace(fallback);
|
||||
setBranch(null);
|
||||
} else if (!fallback && needsWorkspace(name)) {
|
||||
} else if (!fallback) {
|
||||
setWorkspace(null); // orphan cowork: server provisions a fresh scratch on connect
|
||||
}
|
||||
setSessionId(id);
|
||||
@@ -1864,7 +1859,7 @@ export function App() {
|
||||
<span className="mark">✦</span>
|
||||
{agent === "chat" ? "How can I help?" : "Let's build something."}
|
||||
</h1>
|
||||
{needsWorkspace(agent) && (
|
||||
{(
|
||||
<div className="suggestions">
|
||||
<div className="suggest-head">Try a task</div>
|
||||
{SUGGESTIONS.map((s, i) => (
|
||||
@@ -1943,7 +1938,7 @@ export function App() {
|
||||
<SessionSetupRow
|
||||
personas={personas}
|
||||
agent={agent}
|
||||
showFolder={needsWorkspace(agent)}
|
||||
showFolder
|
||||
folderName={workspace && !tempWorkspace ? baseName(workspace) : null}
|
||||
onPickCoworker={pickCoworker}
|
||||
onPickFolder={pickDraftFolder}
|
||||
@@ -2002,7 +1997,7 @@ export function App() {
|
||||
onModeChange={changeMode}
|
||||
onModelChange={changeModel}
|
||||
sessionId={sessionId}
|
||||
workspace={needsWorkspace(agent) ? workspace || "" : undefined}
|
||||
workspace={workspace || ""}
|
||||
unattended={unattended}
|
||||
onUnattendedChange={agent !== "chat" ? toggleUnattended : undefined}
|
||||
prefill={composerPrefill}
|
||||
|
||||
@@ -1030,10 +1030,8 @@ export interface Persona {
|
||||
name: string;
|
||||
icon: string;
|
||||
tagline: string;
|
||||
needs_workspace: boolean;
|
||||
requires_folder: boolean; // folder gate — drives project-scoping
|
||||
builtin: boolean;
|
||||
family: string;
|
||||
workspace: string; // "git" | "project" | "deliverable" | "none" — drives project-scoping
|
||||
tools: string[];
|
||||
enabled: boolean;
|
||||
surfaced: boolean;
|
||||
@@ -1213,7 +1211,7 @@ export interface PersonaDetail {
|
||||
tools: string[];
|
||||
recommended_models: string[];
|
||||
default_permission_mode: string;
|
||||
workspace: string;
|
||||
requires_folder: boolean; // folder gate (workspace-scratch-design.md)
|
||||
recommends: PersonaRecommendation[];
|
||||
default_connections: PersonaDefaultConnection[];
|
||||
}
|
||||
|
||||
@@ -357,11 +357,10 @@ export function PersonaView({
|
||||
<span className="text-faint">Default mode</span> · {detail.default_permission_mode}
|
||||
</div>
|
||||
)}
|
||||
{detail.workspace && (
|
||||
<div>
|
||||
<span className="text-faint">Workspace</span> · {detail.workspace}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="text-faint">Workspace</span> ·{" "}
|
||||
{detail.requires_folder ? "picked folder" : "scratch"}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* management — the controls that left the list page (UX-035) */}
|
||||
|
||||
@@ -24,10 +24,10 @@ function stubFetch(routes: { match: string; method?: string; json: any }[]) {
|
||||
|
||||
const PERSONAS = {
|
||||
personas: [
|
||||
{ id: "cowork", name: "OpenWorker", icon: "cowork", tagline: "general assistant", family: "knowledge", enabled: true, surfaced: true, default: true },
|
||||
{ id: "ops", name: "Ops", icon: "ops", tagline: "incidents, runbooks", family: "code", enabled: true, surfaced: true, default: false },
|
||||
{ id: "code", name: "Code", icon: "code", tagline: "repository work", family: "code", enabled: true, surfaced: true, default: false },
|
||||
{ id: "secret", name: "Disabled One", icon: "cowork", tagline: "off", family: "knowledge", enabled: false, surfaced: false, default: false },
|
||||
{ id: "cowork", name: "OpenWorker", icon: "cowork", tagline: "general assistant", requires_folder: false, enabled: true, surfaced: true, default: true },
|
||||
{ id: "ops", name: "Ops", icon: "ops", tagline: "incidents, runbooks", requires_folder: true, enabled: true, surfaced: true, default: false },
|
||||
{ id: "code", name: "Code", icon: "code", tagline: "repository work", requires_folder: true, enabled: true, surfaced: true, default: false },
|
||||
{ id: "secret", name: "Disabled One", icon: "cowork", tagline: "off", requires_folder: false, enabled: false, surfaced: false, default: false },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ const SURFACES: { key: string; label: string; icon: IconName; cls: string }[] =
|
||||
const surfaceFromPersona = (p: Persona) => ({
|
||||
key: p.id,
|
||||
label: shortPersonaName(p.name, p.id),
|
||||
icon: personaGlyph(p.icon, p.family),
|
||||
icon: personaGlyph(p.icon, p.requires_folder),
|
||||
cls: `ico-${p.icon || "cowork"}`,
|
||||
});
|
||||
|
||||
|
||||
@@ -39,24 +39,24 @@ export function isEmojiIcon(icon?: string): boolean {
|
||||
return !!icon && /[^\x00-\x7F]/.test(icon);
|
||||
}
|
||||
|
||||
export function personaGlyph(icon?: string, family?: string): IconName {
|
||||
export function personaGlyph(icon?: string, folderScoped?: boolean): IconName {
|
||||
if (icon && NAMED.has(icon)) return icon as IconName;
|
||||
if (icon && LEGACY[icon]) return LEGACY[icon];
|
||||
return family === "code" ? "code" : "sparkle";
|
||||
return folderScoped ? "code" : "sparkle";
|
||||
}
|
||||
|
||||
/** Renders a persona's icon: an emoji as text, otherwise the resolved line glyph. */
|
||||
export function PersonaGlyph({
|
||||
icon,
|
||||
family,
|
||||
folderScoped,
|
||||
size = 14,
|
||||
}: {
|
||||
icon?: string;
|
||||
family?: string;
|
||||
folderScoped?: boolean;
|
||||
size?: number;
|
||||
}) {
|
||||
if (isEmojiIcon(icon)) {
|
||||
return <span style={{ fontSize: size, lineHeight: 1 }}>{icon}</span>;
|
||||
}
|
||||
return <Icon name={personaGlyph(icon, family)} size={size} />;
|
||||
return <Icon name={personaGlyph(icon, folderScoped)} size={size} />;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// A persona is "project-scoped" only when it's code-family: an explicit directory the user
|
||||
// picks, sessions grouped by project in the sidebar. Everything else (knowledge, chat) runs on
|
||||
// a transparent per-conversation scratch dir, with real folders added as roots when needed —
|
||||
// no folder gate, ever. (The old workspace enum — git/project/deliverable/none — collapsed
|
||||
// into family; owner decision 2026-07-03, UX-DECISIONS §16.)
|
||||
export function isProjectScoped(p?: { workspace?: string; family?: string }): boolean {
|
||||
return p?.family === "code";
|
||||
// A persona is "project-scoped" when it declares requires_folder: an explicit directory the
|
||||
// user picks, sessions grouped by project in the sidebar. Everything else runs on a transparent
|
||||
// per-conversation scratch dir, with real folders added as roots when needed — no folder gate.
|
||||
// (The old family/workspace-enum pair collapsed into this trait; workspace-scratch-design.md.)
|
||||
export function isProjectScoped(p?: { requires_folder?: boolean }): boolean {
|
||||
return p?.requires_folder === true;
|
||||
}
|
||||
|
||||
// Persona naming: the product is "OpenWorker"; the personas are a "Coworker" family — Coworker
|
||||
|
||||
Reference in New Issue
Block a user