diff --git a/OpenBitFun-Installer/src/i18n/generatedLocaleContract.ts b/OpenBitFun-Installer/src/i18n/generatedLocaleContract.ts index 27ca795775..47eb2b51ed 100644 --- a/OpenBitFun-Installer/src/i18n/generatedLocaleContract.ts +++ b/OpenBitFun-Installer/src/i18n/generatedLocaleContract.ts @@ -64,15 +64,19 @@ export const SHARED_TERMS_BY_APP_LANGUAGE = { "workspace": "Workspace" }, "modes": { - "agentic": "Agentic Mode", "assistant": "Assistant Mode", "expert": "Expert Mode", "review": "Review Mode" }, "agents": { - "code": "Code Session", - "cowork": "Cowork Session", - "claw": "Claw", + "Minimal": "Minimal", + "Standard": "Standard", + "Ultimate": "Ultimate", + "Creative": "Creative", + "Claw": "Assistant", + "Cowork": "Cowork", + "DeepResearch": "Deep Research", + "ComputerUse": "Computer Use", "default": "Default Assistant" }, "tools": { @@ -113,15 +117,19 @@ export const SHARED_TERMS_BY_APP_LANGUAGE = { "workspace": "工作区" }, "modes": { - "agentic": "代理模式", "assistant": "助手模式", "expert": "专家模式", "review": "评审模式" }, "agents": { - "code": "代码会话", - "cowork": "协作会话", - "claw": "Claw", + "Minimal": "极简", + "Standard": "标准", + "Ultimate": "极致", + "Creative": "创造", + "Claw": "助理", + "Cowork": "办公协作", + "DeepResearch": "深度研究", + "ComputerUse": "电脑操作", "default": "默认助手" }, "tools": { @@ -162,15 +170,19 @@ export const SHARED_TERMS_BY_APP_LANGUAGE = { "workspace": "工作區" }, "modes": { - "agentic": "代理模式", "assistant": "助手模式", "expert": "專家模式", "review": "審查模式" }, "agents": { - "code": "程式碼會話", - "cowork": "協作會話", - "claw": "Claw", + "Minimal": "極簡", + "Standard": "標準", + "Ultimate": "極致", + "Creative": "創造", + "Claw": "助理", + "Cowork": "辦公協作", + "DeepResearch": "深度研究", + "ComputerUse": "電腦操作", "default": "預設助手" }, "tools": { diff --git a/design-system/packages/ui/README.md b/design-system/packages/ui/README.md index 5b3b420a09..d4699d706f 100644 --- a/design-system/packages/ui/README.md +++ b/design-system/packages/ui/README.md @@ -35,6 +35,12 @@ text buttons keep those hit targets while omitting the visible pill background and radius. Use native hover, pressed, focus, disabled, and loading behavior in addition to Design Lab's state specimens. +Hover and press feedback keeps controls at their resting size on desktop and +mobile. Use semantic surface, border, content, shadow, and opacity states; +do not add press scaling in components or host-wide semantic selectors. +Explicit content zoom, loading indicators, and enter/exit motion remain owned +by the component that needs them. + ## Native scrollbars `styles.css` owns scrollbar presentation inside `ThemeRoot` (or diff --git a/design-system/packages/ui/src/mobile/MobileButton/MobileButton.module.css b/design-system/packages/ui/src/mobile/MobileButton/MobileButton.module.css index 28c39e613d..eb77c8201c 100644 --- a/design-system/packages/ui/src/mobile/MobileButton/MobileButton.module.css +++ b/design-system/packages/ui/src/mobile/MobileButton/MobileButton.module.css @@ -19,7 +19,7 @@ font: inherit; font-size: var(--openbitfun-type-label-md-font-size); font-weight: var(--openbitfun-type-label-md-font-weight); - transition: background-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), transform var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); + transition: background-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); } .root[data-size="sm"] { min-block-size: var(--openbitfun-space-12); padding-inline: var(--openbitfun-space-3); font-size: var(--openbitfun-type-label-sm-font-size); } @@ -33,7 +33,7 @@ .root:hover:not(:disabled) { background: var(--openbitfun-color-action-neutral-surface-hover); box-shadow: var(--openbitfun-shadow-sm); } .root[data-appearance="primary"]:hover:not(:disabled) { background: var(--openbitfun-color-action-primary-hover); } } - .root:active:not(:disabled) { background: var(--openbitfun-color-action-neutral-surface-pressed); transform: scale(0.98); } + .root:active:not(:disabled) { background: var(--openbitfun-color-action-neutral-surface-pressed); } .root[data-appearance="primary"]:active:not(:disabled) { background: var(--openbitfun-color-action-primary-pressed); } .root:focus-visible { outline: var(--openbitfun-focus-width) solid var(--openbitfun-color-focus-ring); outline-offset: var(--openbitfun-focus-offset); } .root:disabled { color: var(--openbitfun-color-action-neutral-content-disabled); cursor: not-allowed; box-shadow: none; } diff --git a/design-system/packages/ui/src/mobile/MobileComposer/MobileComposer.module.css b/design-system/packages/ui/src/mobile/MobileComposer/MobileComposer.module.css index 6ada197aaa..6d0bcdeb96 100644 --- a/design-system/packages/ui/src/mobile/MobileComposer/MobileComposer.module.css +++ b/design-system/packages/ui/src/mobile/MobileComposer/MobileComposer.module.css @@ -18,8 +18,7 @@ transition: min-block-size var(--openbitfun-motion-duration-base) var(--openbitfun-motion-easing-standard), border-radius var(--openbitfun-motion-duration-base) var(--openbitfun-motion-easing-standard), - box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - transform var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); + box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); } .root[data-interactive="true"] { @@ -29,7 +28,6 @@ } .root[data-interactive="true"]:active { - transform: scale(0.99); box-shadow: var(--openbitfun-shadow-sm); } diff --git a/design-system/packages/ui/src/mobile/MobileIconButton/MobileIconButton.module.css b/design-system/packages/ui/src/mobile/MobileIconButton/MobileIconButton.module.css index 1d00670e63..35066c8e0b 100644 --- a/design-system/packages/ui/src/mobile/MobileIconButton/MobileIconButton.module.css +++ b/design-system/packages/ui/src/mobile/MobileIconButton/MobileIconButton.module.css @@ -24,8 +24,7 @@ color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), background-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - transform var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); + box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); } .root[data-size="sm"] { @@ -58,7 +57,6 @@ .root:is(:active, [data-openbitfun-preview-state="active"]):not(:disabled) { color: var(--openbitfun-color-content-primary); background: var(--openbitfun-color-action-neutral-surface-pressed); - transform: scale(0.94); } .root:is(:focus-visible, [data-openbitfun-preview-state="focus-visible"]) { diff --git a/design-system/packages/ui/src/mobile/MobileListRow/MobileListRow.module.css b/design-system/packages/ui/src/mobile/MobileListRow/MobileListRow.module.css index 55f1d40304..7c9881270f 100644 --- a/design-system/packages/ui/src/mobile/MobileListRow/MobileListRow.module.css +++ b/design-system/packages/ui/src/mobile/MobileListRow/MobileListRow.module.css @@ -20,8 +20,7 @@ transition: background-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - transform var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); + box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); } .root[data-appearance="surface"] { @@ -44,7 +43,6 @@ .root:is(:active, [data-openbitfun-preview-state="active"]):not(:disabled) { background: var(--openbitfun-color-action-neutral-surface-pressed); - transform: scale(0.99); } .root:is(:focus-visible, [data-openbitfun-preview-state="focus-visible"]) { diff --git a/docs/architecture/product-architecture.md b/docs/architecture/product-architecture.md index b8a0bd1be3..6d56130209 100644 --- a/docs/architecture/product-architecture.md +++ b/docs/architecture/product-architecture.md @@ -911,3 +911,28 @@ WorkspaceInfo/WorkspaceIdentity 的运行操作由 Core 的 runtime extension tr 原 Core 存储入口保留错误映射;可选 legacy-migration facade 保留旧导入路径,但不再由 product-full 启用。 远程四种场景不提供迁移工具的执行入口;仅转换本机保存的连接记录,不连接远端。 使用与发行契约以 [独立迁移器说明](../../src/apps/data-migrator/README.zh-CN.md) 为准。 + + +## Agent 身份与产品概念 + +Agent Harness 的唯一规范标识为 `Minimal`、`Standard`、`Ultimate`、`Creative`,分别显示为极简、标准、极致、创造。配置键、运行注册、会话状态和新持久化记录共用这些大小写固定的标识。共享源为 [Harness 身份契约](../../src/shared/agent-harness/contract.json),由 `pnpm run agent-harness:generate` 生成 Rust 和 TypeScript 类型,核心边界检查校验生成结果。 + +| 概念 | 负责的事实 | 例子 | +| --- | --- | --- | +| Agent Harness | 会话的执行策略、工具策略和协作方式 | Minimal、Standard、Ultimate、Creative | +| 智能体(Agent) | 直接承接用户任务的角色 | 助理 Claw、办公协作 Cowork、深度研究 DeepResearch,以及自定义智能体 | +| 子智能体(SubAgent) | 受父级委派的独立任务执行者 | ComputerUse、Explore、ResearchSpecialist、SwarmWorker | +| 技能(Skill) | 执行者可发现和调用的能力资源 | 由技能注册表提供 | +| 技能套件 | 现有的技能分组与各执行者的技能选择配置 | 保留分组、开关、逐项调整、按组保存和重置 | + +来源(内置、用户、项目、外部)与角色分别建模。电脑操作属于子智能体;助理可在智能体管理页配置,但会话选择器继续遵守助理工作区的固定入口。技能套件从当前设备、工作区的真实目录中筛选支持 Skill 工具且可由本产品配置的执行者,不维护三项固定名单,不为缺少 Skill 能力的 Minimal 提供无效配置,也不将外部来源的只读定义当成本地可写配置。 + +会话选择器保留四项 Harness 和既有智能体子菜单。打开、选择、关闭、高亮、首轮锁定、新会话确认、跟随上次选择、固定默认值,以及助理、ACP、子会话限制均由原有交互规则负责。`other` 只代表菜单导航,不能写入运行身份。一个主会话选择一个可执行入口;Harness 和普通智能体不组成无依据的二重配置。 + +旧名只在明确的升级与协议适配边界读取。历史 `coding_shared` 配置键迁往 Standard,迁移同时保留工具、技能及子智能体覆盖;无法无损合并的冲突返回明确错误并保留原始文件。旧会话中的 Minimal 执行标记保留原有优先级。外部或带来源限定的身份不改名,未知身份保留并交由原有可用性判断处理。 + +Peer HostInvoke、Peer 事件和移动遥控在各自适配边界继续支持现有 v0 线协议,双向翻译产品身份字段;应用内状态和磁盘记录保持规范标识。工具参数、提示词正文、用户元数据和外部定义不参与通用翻译。旧 CLI 参数 `--harness-profile balanced` 的历史语义仍是保留显式 `--agent`,先解码语义再规范化身份。各发行能力集继续决定真正可执行的目录,命名统一不增加 CLI 或远程宿主的能力。 + +历史 API 名称(例如 get_available_modes)、agentic 事件通道和编译期提示词资源键是兼容协议,不是另一套 Agent 标识。小写图标、颜色 token 和模型推理强度属于各自独立领域,不通过大小写替换改变其语义。技能“已启用”或“运行时已选择”表示可用性与解析状态,不能表述为已经被 Agent 调用。 + +Detached Dispatch 的 SSH、账号设备 RPC 和 CLI 协议入口同样在传输边界保留现有协议标识,接收后立即规范化。目标持久化任务、会话和控制端观察记录使用规范身份;改名不改变协议版本、工作区归属或断线后目标独立执行的行为。按路径读写配置时,适配器只转换对应身份字段,保留未知配置字段;歧义别名在调用写入逻辑之前返回错误。 diff --git a/package.json b/package.json index 111c68a395..4ebaf6fde7 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "node": ">=22.12.0" }, "scripts": { + "agent-harness:generate": "node scripts/generate-agent-harness.mjs", + "agent-harness:check": "node scripts/generate-agent-harness.mjs --check", "copy-monaco": "copyfiles -u 5 \"src/web-ui/node_modules/monaco-editor/min/vs/**/*\" src/web-ui/public/monaco-editor && node scripts/prune-monaco-nls.cjs", "generate-brand-assets": "node scripts/generate-brand-assets.mjs", "copy-assets": "pnpm run copy-monaco && pnpm run generate-brand-assets", @@ -73,7 +75,7 @@ "mobile:ui:generate": "node scripts/mobile-ui-design-system.mjs", "mobile:ui:check": "node scripts/mobile-ui-design-system.mjs --check && node scripts/mobile-ui-preview.mjs --check", "mobile:ui:preview": "node scripts/mobile-ui-design-system.mjs && node scripts/mobile-ui-preview.mjs", - "check:core-boundaries": "node scripts/check-core-boundaries.mjs", + "check:core-boundaries": "pnpm run agent-harness:check && node scripts/check-core-boundaries.mjs", "check:core-boundaries:test": "node --test scripts/check-core-boundaries.test.mjs", "check:github-config": "pnpm --dir src/web-ui exec node ../../scripts/check-github-config.mjs && node --test scripts/check-github-config.test.mjs", "test:release-packaging": "node --test scripts/release-channel.test.mjs scripts/desktop-tauri-build.test.mjs scripts/server-build.test.mjs scripts/version-generation.test.mjs scripts/generate-frontend-revision.test.mjs scripts/generate-brand-assets.test.mjs scripts/tauri-release-manifest.test.mjs OpenBitFun-Installer/scripts/build-installer.test.cjs", diff --git a/scripts/check-core-boundaries.test.mjs b/scripts/check-core-boundaries.test.mjs index d72dfb3747..67aeeadadd 100644 --- a/scripts/check-core-boundaries.test.mjs +++ b/scripts/check-core-boundaries.test.mjs @@ -862,6 +862,7 @@ test('contract and AI adapter tests keep reviewed feature and failure-domain top name: 'core_type_contracts', path: 'tests/core_type_contracts.rs', leaves: [ + 'tests/core_type_contracts/agent_identity.rs', 'tests/core_type_contracts/session_contracts.rs', 'tests/core_type_contracts/session_usage_contracts.rs', 'tests/core_type_contracts/surface_contracts.rs', diff --git a/scripts/core-boundaries/explicit-test-topology.mjs b/scripts/core-boundaries/explicit-test-topology.mjs index d3026092bb..e3c93aa5ec 100644 --- a/scripts/core-boundaries/explicit-test-topology.mjs +++ b/scripts/core-boundaries/explicit-test-topology.mjs @@ -129,6 +129,7 @@ export const coreTypesIntegrationTestTargets = [ name: 'core_type_contracts', path: 'tests/core_type_contracts.rs', leaves: [ + 'tests/core_type_contracts/agent_identity.rs', 'tests/core_type_contracts/session_contracts.rs', 'tests/core_type_contracts/session_usage_contracts.rs', 'tests/core_type_contracts/surface_contracts.rs', diff --git a/scripts/core-boundaries/rules/feature-rules.mjs b/scripts/core-boundaries/rules/feature-rules.mjs index 5ee5bb6226..5a9e4966ca 100644 --- a/scripts/core-boundaries/rules/feature-rules.mjs +++ b/scripts/core-boundaries/rules/feature-rules.mjs @@ -275,7 +275,7 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'openbitfun-agent-workflows', ownerFeatures: ['deep-research'] }, { depName: 'openbitfun-core-types', - ownerFeatures: ['deep-research', 'remote-connect', 'speech'], + ownerFeatures: ['deep-research', 'remote-connect', 'remote-ssh-concrete', 'speech'], }, { depName: 'openbitfun-product-domains', ownerFeatures: ['account-identity', 'canvas-runtime', 'function-agents', 'hook-import', 'miniapp-market', 'miniapp-runtime', 'miniapp-storage', 'plugin-source', 'remote-connect'] }, { depName: 'openbitfun-runtime-ports', ownerFeatures: ['deep-research', 'git', 'remote-connect', 'remote-ssh', 'remote-ssh-concrete', 'script-tool-runtime', 'web-tools'] }, diff --git a/scripts/core-boundaries/rules/source/forbidden-rules.mjs b/scripts/core-boundaries/rules/source/forbidden-rules.mjs index 595bbf5439..d62c549797 100644 --- a/scripts/core-boundaries/rules/source/forbidden-rules.mjs +++ b/scripts/core-boundaries/rules/source/forbidden-rules.mjs @@ -1568,19 +1568,19 @@ export const forbiddenContentRules = [ path: 'src/crates/assembly/core/src/agentic/agents/mod.rs', patterns: [ { - regex: /\bpub const SHARED_CODING_MODE_PROMPT_TEMPLATE\b/, + regex: /\bpub const STANDARD_HARNESS_PROMPT_TEMPLATE\b/, message: - 'core agent mode module must not own shared coding-mode prompt facts; use openbitfun-agent-runtime agents', + 'core agent mode module must not own Standard Harness prompt facts; use openbitfun-agent-runtime agents', }, { - regex: /\bpub const SHARED_CODING_MODE_CONFIG_PROFILE_ID\b/, + regex: /\bpub const STANDARD_HARNESS_CONFIG_ID\b/, message: - 'core agent mode module must not own shared coding-mode config profile facts; use openbitfun-agent-runtime agents', + 'core agent mode module must not own Standard Harness config profile facts; use openbitfun-agent-runtime agents', }, { - regex: /\bpub const SHARED_CODING_MODE_IDS\b/, + regex: /\bpub const STANDARD_HARNESS_CONFIG_MEMBERS\b/, message: - 'core agent mode module must not own shared coding-mode membership facts; use openbitfun-agent-runtime agents', + 'core agent mode module must not own Standard Harness membership facts; use openbitfun-agent-runtime agents', }, { regex: /\bpub fn resolve_mode_config_profile_id\b/, @@ -1598,9 +1598,9 @@ export const forbiddenContentRules = [ 'core agent mode module must not own mode config profile labels; use openbitfun-agent-runtime agents', }, { - regex: /\bpub fn shared_coding_mode_user_context_policy\b/, + regex: /\bpub fn standard_harness_user_context_policy\b/, message: - 'core agent mode module must not own shared coding-mode context policy; use openbitfun-agent-runtime agents', + 'core agent mode module must not own Standard Harness context policy; use openbitfun-agent-runtime agents', }, ], }, diff --git a/scripts/core-boundaries/rules/source/required-rules.mjs b/scripts/core-boundaries/rules/source/required-rules.mjs index 8281f96ae9..befda8e0a2 100644 --- a/scripts/core-boundaries/rules/source/required-rules.mjs +++ b/scripts/core-boundaries/rules/source/required-rules.mjs @@ -1506,12 +1506,12 @@ export const requiredContentRules = [ 'agent-runtime must own shared mode config profile facts that are runtime-visible and product-neutral', patterns: [ { - regex: /\bpub const SHARED_CODING_MODE_PROMPT_TEMPLATE\b/, - message: 'missing shared coding-mode prompt template fact', + regex: /\bpub const STANDARD_HARNESS_PROMPT_TEMPLATE\b/, + message: 'missing Standard Harness prompt template fact', }, { - regex: /\bpub const SHARED_CODING_MODE_CONFIG_PROFILE_ID\b/, - message: 'missing shared coding-mode config profile id', + regex: /\bpub const STANDARD_HARNESS_CONFIG_ID\b/, + message: 'missing Standard Harness config profile id', }, { regex: /\bpub fn resolve_mode_config_profile_id\b/, @@ -1526,8 +1526,8 @@ export const requiredContentRules = [ message: 'missing mode presentation rank', }, { - regex: /\bpub fn shared_coding_mode_user_context_policy\b/, - message: 'missing shared coding-mode user-context policy', + regex: /\bpub fn standard_harness_user_context_policy\b/, + message: 'missing Standard Harness user-context policy', }, { regex: /\bpub enum SubagentListScope\b/, @@ -1593,7 +1593,7 @@ export const requiredContentRules = [ }, { regex: /\bshared_coding_modes_resolve_to_the_same_config_profile\b/, - message: 'missing shared coding-mode profile regression', + message: 'missing Standard Harness profile regression', }, { regex: @@ -3111,7 +3111,7 @@ export const requiredContentRules = [ 'core agent mode module must keep old import paths while agent-runtime owns shared mode profile facts', patterns: [ { - regex: /pub use openbitfun_agent_runtime::agents::\{[\s\S]*mode_presentation_rank[\s\S]*resolve_mode_config_profile_id[\s\S]*shared_coding_mode_user_context_policy[\s\S]*SHARED_CODING_MODE_PROMPT_TEMPLATE[\s\S]*\};/, + regex: /pub use openbitfun_agent_runtime::agents::\{[\s\S]*mode_presentation_rank[\s\S]*resolve_mode_config_profile_id[\s\S]*standard_harness_user_context_policy[\s\S]*STANDARD_HARNESS_PROMPT_TEMPLATE[\s\S]*\};/, message: 'missing agent-runtime shared mode profile compatibility re-export', }, ], diff --git a/scripts/core-boundaries/self-test.mjs b/scripts/core-boundaries/self-test.mjs index 05be44e779..76a000757c 100644 --- a/scripts/core-boundaries/self-test.mjs +++ b/scripts/core-boundaries/self-test.mjs @@ -2833,11 +2833,11 @@ export function runManifestParserSelfTest({ 'resolve_subagent_availability', 'SubagentOverrideLayers', 'SubagentStateReason', - 'SHARED_CODING_MODE_CONFIG_PROFILE_ID', + 'STANDARD_HARNESS_CONFIG_ID', 'resolve_mode_config_profile_id', 'mode_config_profile_member_mode_ids', 'mode_presentation_rank', - 'shared_coding_mode_user_context_policy', + 'standard_harness_user_context_policy', 'SubAgentSource', 'subagent_source_kind', 'subagent_source_presentation_rank', diff --git a/scripts/generate-agent-harness.mjs b/scripts/generate-agent-harness.mjs new file mode 100644 index 0000000000..09b5ee14bf --- /dev/null +++ b/scripts/generate-agent-harness.mjs @@ -0,0 +1,115 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const contract = JSON.parse(fs.readFileSync(path.join(root, 'src/shared/agent-harness/contract.json'), 'utf8')); +const entries = contract.harnesses; +const quote = JSON.stringify; +const banner = '// Generated by scripts/generate-agent-harness.mjs. Edit the shared contract instead.\n'; +const ts = `${banner} +export const HARNESS_IDS = [${entries.map(entry => quote(entry.id)).join(', ')}] as const; +export type HarnessId = typeof HARNESS_IDS[number]; +const aliases: ReadonlyMap = new Map([ +${entries.flatMap(entry => [entry.id, ...entry.legacyIds].map(id => ` [${quote(id)}, ${quote(entry.id)}],`)).join('\n')} +]); +const legacyWireIds: Record = { +${entries.map(entry => ` ${entry.id}: ${quote(entry.legacyWireId)},`).join('\n')} +}; +const configAliases: ReadonlyMap = new Map([ +${entries.flatMap(entry => (entry.legacyConfigIds ?? []).map(id => ` [${quote(id)}, ${quote(entry.id)}],`)).join('\n')} +]); +export function isHarnessId(id: string): id is HarnessId { + return (HARNESS_IDS as readonly string[]).includes(id); +} +/** Use only at a built-in identity boundary; source-qualified/custom IDs stay intact. */ +export function canonicalHarnessId(id: string | null | undefined): HarnessId | null { + const key = id?.trim(); + return key ? aliases.get(key) ?? aliases.get(key.toLowerCase()) ?? null : null; +} +export function canonicalAgentId(id: string): string { + return canonicalHarnessId(id) ?? id; +} +export function legacyAgentWireId(id: string): string { + const harness = canonicalHarnessId(id); + return harness ? legacyWireIds[harness] : id; +} +export function canonicalAgentConfigId(id: string): string { + return configAliases.get(id) ?? canonicalAgentId(id); +} +`; +const rs = `${banner} +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum HarnessId { +${entries.map(entry => ` ${entry.legacyIds.map(id => `#[serde(alias = ${quote(id)})]`).join('\n ')}\n ${entry.id},`).join('\n')} +} + +impl HarnessId { + pub const ALL: [Self; ${entries.length}] = [ +${entries.map(entry => ` Self::${entry.id},`).join('\n')} + ]; + pub const fn as_str(self) -> &'static str { + match self { +${entries.map(entry => ` Self::${entry.id} => ${quote(entry.id)},`).join('\n')} + } + } + pub fn from_legacy_id(id: &str) -> Option { + match id.trim().to_ascii_lowercase().as_str() { +${entries.map(entry => ` ${[...new Set([entry.id, ...entry.legacyIds].map(id => id.toLowerCase()))].map(quote).join(' | ')} => Some(Self::${entry.id}),`).join('\n')} + _ => None, + } + } + pub const fn legacy_wire_id(self) -> &'static str { + match self { +${entries.map(entry => ` Self::${entry.id} => ${quote(entry.legacyWireId)},`).join('\n')} + } + } + pub const fn legacy_ids(self) -> &'static [&'static str] { + match self { +${entries.map(entry => ` Self::${entry.id} => &[${entry.legacyIds.map(quote).join(', ')}],`).join('\n')} + } + } +} + +/// Normalize a built-in identity at storage/protocol boundaries. Unknown IDs survive. +pub fn canonical_agent_id(id: &str) -> &str { + HarnessId::from_legacy_id(id) + .map(HarnessId::as_str) + .unwrap_or(id) +} + +pub fn canonical_agent_config_id(id: &str) -> &str { + match id { +${entries.flatMap(entry => (entry.legacyConfigIds ?? []).map(id => ` ${quote(id)} => ${quote(entry.id)},`)).join('\n')} + _ => canonical_agent_id(id), + } +} + +pub fn deserialize_agent_id<'de, D: serde::Deserializer<'de>>( + deserializer: D, +) -> Result { + let id = String::deserialize(deserializer)?; + Ok(canonical_agent_id(&id).to_owned()) +} + +pub fn deserialize_optional_agent_id<'de, D: serde::Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + Ok(Option::::deserialize(deserializer)?.map(|id| canonical_agent_id(&id).to_owned())) +} +`; +for (const [relative, content] of [ + ['src/shared/agent-harness/contract.generated.ts', ts], + ['src/crates/contracts/core-types/src/agent_identity.rs', rs], +]) { + const output = path.join(root, relative); + if (process.argv.includes('--check')) { + if (!fs.existsSync(output) || fs.readFileSync(output, 'utf8').replace(/\r\n/g, '\n') !== content) { + throw new Error(`Generated Harness contract is stale: ${relative}`); + } + } else { + fs.writeFileSync(output, content); + } +} diff --git a/scripts/i18n-dynamic-key-allowlist.json b/scripts/i18n-dynamic-key-allowlist.json index a761a41187..716d8ecb8e 100644 --- a/scripts/i18n-dynamic-key-allowlist.json +++ b/scripts/i18n-dynamic-key-allowlist.json @@ -165,21 +165,17 @@ ] }, { - "id": "web-skills-filter-and-suite-metadata", + "id": "web-skills-filter-metadata", "surface": "web-ui", "namespace": "scenes/skills", "owner": "src/web-ui/src/app/scenes/skills", - "description": "Skills filters and suite modes store labelKey and descKey metadata resolved with the scenes/skills namespace.", + "description": "Skill library filters and group navigation resolve their labels from the owning scene.", "sourceReferences": [ - "labelKey: 'filters.all'", - "labelKey: 'suite.modes.agentic'", - "descKey: 'suite.modeDescriptions.agentic'" + "labelKey: 'filters.all'" ], "keyPrefixes": [ "filters.", - "categories.", - "suite.modes.", - "suite.modeDescriptions." + "categories." ] } ] diff --git a/scripts/i18n-governance-baseline.json b/scripts/i18n-governance-baseline.json index 9a8eb85de1..9f0bc2b82a 100644 --- a/scripts/i18n-governance-baseline.json +++ b/scripts/i18n-governance-baseline.json @@ -1,24 +1,24 @@ { "version": 1, - "description": "No-growth baseline for i18n governance candidates. Lower counts when shared-term or l10n debt is removed; do not raise without review.", + "description": "No-growth baseline for i18n governance candidates. Lower counts when shared-term or l10n debt is removed; do not raise without review. Agent identity unification removes duplicate labels; agents.Minimal and agents.Standard only inventory existing reasoning, review, and Markdown-preview homonyms.", "budgets": { "confirmedUnusedKeys": { "maxTotal": 0 }, "sharedTermDuplicates": { - "maxTotal": 168, + "maxTotal": 154, "bySurface": { "core": 15, "installer": 0, "mobile-web": 0, "relay-static-homepage": 0, - "web-ui": 153 + "web-ui": 139 }, "bySharedKey": { - "agents.claw": 3, + "agents.claw": 0, "agents.code": 0, "agents.cowork": 0, - "agents.default": 2, + "agents.default": 0, "connectionMethods.openbitfunServer": 0, "connectionMethods.lan": 0, "features.codeAgent": 1, @@ -30,15 +30,17 @@ "modes.expert": 2, "modes.review": 0, "product.name": 0, - "statuses.cancelled": 27, - "statuses.done": 24, - "statuses.failed": 39, + "statuses.cancelled": 24, + "statuses.done": 20, + "statuses.failed": 32, "statuses.loading": 5, - "statuses.running": 10, + "statuses.running": 6, "tools.edit": 33, "tools.explore": 2, "tools.search": 10, - "tools.shell": 7 + "tools.shell": 7, + "agents.Minimal": 3, + "agents.Standard": 6 } }, "l10nQualityCandidates": { diff --git a/src/apps/cli/src/agent/runtime_client.rs b/src/apps/cli/src/agent/runtime_client.rs index 558a9a3a66..be21a30dc7 100644 --- a/src/apps/cli/src/agent/runtime_client.rs +++ b/src/apps/cli/src/agent/runtime_client.rs @@ -2668,7 +2668,7 @@ mod tests { AgentSessionSummary { session_id: session_id.to_string(), session_name: "Workspace session".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), model_id: None, reasoning_preset: None, last_user_dialog_agent_type: None, @@ -2723,7 +2723,7 @@ mod tests { notices, vec![SessionMigrationNotice::Mode { previous_id: "removed-mode".to_string(), - restored_id: "agentic".to_string(), + restored_id: "Standard".to_string(), }] ); } @@ -2769,7 +2769,7 @@ mod tests { project_path: None, project_id: "project".to_string(), session_id: "child".to_string(), - agent_id: "agentic".to_string(), + agent_id: "Standard".to_string(), action: "run command".to_string(), resources: Vec::new(), save_resources: Vec::new(), @@ -2903,8 +2903,8 @@ mod dual_backend_behavior_tests { fn embedded_modes() -> Vec { vec![ AgentModeCatalogEntry { - id: "agentic".to_string(), - route_key: "agentic".to_string(), + id: "Standard".to_string(), + route_key: "Standard".to_string(), description: "Primary workspace agent".to_string(), model_id: Some("primary-model".to_string()), is_external: false, @@ -3437,7 +3437,7 @@ mod dual_backend_behavior_tests { project_path: None, project_id: "project-1".to_string(), session_id: session_id.to_string(), - agent_id: "agentic".to_string(), + agent_id: "Standard".to_string(), action: "edit".to_string(), resources: vec!["src/main.rs".to_string()], save_resources: vec!["src/main.rs".to_string()], @@ -3456,7 +3456,7 @@ mod dual_backend_behavior_tests { ) -> ScenarioSnapshot { let modes = client.available_agent_modes().await.expect("modes"); let created_id = client - .create_new_session("agentic") + .create_new_session("Standard") .await .expect("create session"); let listed = client.list_sessions().await.expect("list sessions"); @@ -3476,7 +3476,7 @@ mod dual_backend_behavior_tests { .find(|session| session.session_id == created_id); let turn_id = client - .send_message("hello".to_string(), "agentic") + .send_message("hello".to_string(), "Standard") .await .expect("send message"); client @@ -3566,7 +3566,7 @@ mod dual_backend_behavior_tests { fixture.state.insert_session( "orphan-session".to_string(), "orphan".to_string(), - "agentic".to_string(), + "Standard".to_string(), ); client .delete_session("orphan-session") @@ -3611,9 +3611,9 @@ mod dual_backend_behavior_tests { assert_eq!(embedded, shared); assert_eq!( embedded.mode_ids, - ["agentic".to_string(), "workspace-plan".to_string()] + ["Standard".to_string(), "workspace-plan".to_string()] ); - assert_eq!(embedded.created_session_agent_type, "agentic"); + assert_eq!(embedded.created_session_agent_type, "Standard"); assert_eq!(embedded.listed_session_count, 1); assert_eq!( embedded.renamed_session_name.as_deref(), @@ -3626,7 +3626,7 @@ mod dual_backend_behavior_tests { assert!(embedded.permission_cleared); assert_eq!( embedded.restore_session_agent_type.as_deref(), - Some("agentic") + Some("Standard") ); assert_eq!(embedded.restore_transcript_messages, 0); assert_eq!(embedded.event_states, ["first", "second"]); @@ -3653,11 +3653,11 @@ mod dual_backend_behavior_tests { } let session_id = client - .create_new_session("agentic") + .create_new_session("Standard") .await .expect("create session"); let turn_id = client - .send_message("hello".to_string(), "agentic") + .send_message("hello".to_string(), "Standard") .await .expect("send message"); fixture.state.settlement_outcomes.lock().unwrap().insert( @@ -3691,7 +3691,7 @@ mod dual_backend_behavior_tests { let create_request = AgentSessionCreateRequest { session_name: "remote-unsupported-session".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), agent_route_key: None, workspace_path: Some(fixture.workspace.to_string_lossy().into_owned()), project_workspace_path: Some(fixture.workspace.to_string_lossy().into_owned()), diff --git a/src/apps/cli/src/chat_state.rs b/src/apps/cli/src/chat_state.rs index db6d355131..82752b400f 100644 --- a/src/apps/cli/src/chat_state.rs +++ b/src/apps/cli/src/chat_state.rs @@ -1662,7 +1662,7 @@ mod tests { let mut state = ChatState::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("/tmp/project".to_string()), ); state.set_git_repository_status(true, Some("main".to_string())); @@ -1686,7 +1686,7 @@ mod tests { let mut state = ChatState::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("/tmp/project".to_string()), ); state.add_system_message("Worktree: off".to_string()); @@ -1701,7 +1701,7 @@ mod tests { let mut state = ChatState::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("/tmp/project".to_string()), ); let usage = ModelTokenUsageSnapshot { @@ -1726,7 +1726,7 @@ mod tests { let mut state = ChatState::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("/tmp/project".to_string()), ); state.set_git_repository_status(true, Some("main".to_string())); @@ -1746,7 +1746,7 @@ mod tests { let mut state = ChatState::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("/tmp/project".to_string()), ); state.apply_workspace_binding(managed_worktree_binding(Some("feature/test"), None)); @@ -1816,7 +1816,7 @@ mod tests { let mut state = ChatState::new( "parent-session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), None, ); let first = permission_request("request-z", "child-a"); @@ -1870,7 +1870,7 @@ mod tests { let mut state = ChatState::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), None, ); let first = PermissionRequest { @@ -1923,7 +1923,7 @@ mod tests { let mut state = ChatState::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), None, ); state.handle_turn_started("turn-1", "Create a plan"); @@ -1951,7 +1951,7 @@ mod tests { let mut state = ChatState::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), None, ); state.handle_turn_started("turn-1", "Start the task"); @@ -2012,7 +2012,7 @@ mod tests { let state = ChatState::from_session_transcript( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), None, &transcript, ); @@ -2039,7 +2039,7 @@ mod tests { let mut state = ChatState::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/project".to_string()), ); state.current_model_id = Some("model-1".to_string()); @@ -2071,7 +2071,7 @@ mod tests { let mut state = ChatState::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), None, ); let transcript = SessionTranscript { @@ -2139,7 +2139,7 @@ mod tests { let state = ChatState::from_session_transcript( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), None, &transcript, ); @@ -2187,7 +2187,7 @@ mod tests { let state = ChatState::from_session_transcript( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), None, &transcript, ); @@ -2240,7 +2240,7 @@ mod tests { let state = ChatState::from_session_transcript( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), None, &transcript, ); diff --git a/src/apps/cli/src/config.rs b/src/apps/cli/src/config.rs index f7ae835c8b..efd84cab9a 100644 --- a/src/apps/cli/src/config.rs +++ b/src/apps/cli/src/config.rs @@ -135,7 +135,7 @@ impl Default for BehaviorConfig { Self { auto_save: true, confirm_dangerous: true, - default_agent: "agentic".to_string(), + default_agent: "Standard".to_string(), auto_update: true, } } @@ -298,7 +298,7 @@ mod tests { assert_eq!(config.ui.notification_method, NotificationMethod::Auto); assert!(config.behavior.auto_save); assert!(config.behavior.confirm_dangerous); - assert_eq!(config.behavior.default_agent, "agentic"); + assert_eq!(config.behavior.default_agent, "Standard"); assert!(config.behavior.auto_update); assert_eq!(config.workspace.default_path, "."); assert_eq!( diff --git a/src/apps/cli/src/dispatch/mod.rs b/src/apps/cli/src/dispatch/mod.rs index 7a14ede638..64e81fe3ae 100644 --- a/src/apps/cli/src/dispatch/mod.rs +++ b/src/apps/cli/src/dispatch/mod.rs @@ -46,6 +46,25 @@ impl ModelReadiness { pub(crate) async fn run_dispatch_verb( verb: &str, input: serde_json::Value, +) -> Result { + use openbitfun_core_types::agent_identity_wire::{ + translate_agent_identity_command, translate_agent_identity_response, AgentIdentityDialect, + }; + let input = translate_agent_identity_command(verb, input, AgentIdentityDialect::Canonical) + .map_err(anyhow::Error::msg)?; + let result = run_dispatch_verb_canonical(verb, input).await?; + translate_agent_identity_response( + verb, + result, + &serde_json::Value::Null, + AgentIdentityDialect::Legacy, + ) + .map_err(anyhow::Error::msg) +} + +async fn run_dispatch_verb_canonical( + verb: &str, + input: serde_json::Value, ) -> Result { match verb { "probe" => { @@ -1054,7 +1073,7 @@ mod tests { job_id: job_id.to_string(), session_id: format!("session-{job_id}"), workspace_path: "/tmp/workspace".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), prompt: "task".to_string(), approval_policy: DispatchApprovalPolicy::RejectAndReport, model: Some("model-1".to_string()), @@ -1072,7 +1091,7 @@ mod tests { "jobId": "job-1", "sessionId": "session-1", "workspacePath": "/tmp/workspace", - "agentType": "agentic", + "agentType": "Standard", "prompt": "task" }); assert!(parse::(missing).is_err()); @@ -1082,7 +1101,7 @@ mod tests { "jobId": "job-1", "sessionId": "session-1", "workspacePath": "/tmp/workspace", - "agentType": "agentic", + "agentType": "Standard", "prompt": "task", "approvalPolicy": "reject-and-report" })) @@ -1096,7 +1115,7 @@ mod tests { "jobId": "job-1", "sessionId": "session-1", "workspacePath": "/tmp/workspace", - "agentType": "agentic", + "agentType": "Standard", "prompt": "task", "approvalPolicy": "reject-and-report" }); diff --git a/src/apps/cli/src/dispatch/protocol.rs b/src/apps/cli/src/dispatch/protocol.rs index a6b6611c27..8be0920ced 100644 --- a/src/apps/cli/src/dispatch/protocol.rs +++ b/src/apps/cli/src/dispatch/protocol.rs @@ -87,6 +87,7 @@ pub(crate) struct DispatchSubmitRequest { pub(crate) job_id: String, pub(crate) session_id: String, pub(crate) workspace_path: String, + #[serde(deserialize_with = "openbitfun_core_types::agent_identity::deserialize_agent_id")] pub(crate) agent_type: String, pub(crate) prompt: String, pub(crate) approval_policy: DispatchApprovalPolicy, @@ -630,6 +631,7 @@ pub(crate) struct DispatchJobListEntry { pub(crate) started_at: Option, pub(crate) workspace_path: String, pub(crate) title: String, + #[serde(deserialize_with = "openbitfun_core_types::agent_identity::deserialize_agent_id")] pub(crate) agent_type: String, pub(crate) approval_policy: DispatchApprovalPolicy, #[serde(skip_serializing_if = "Option::is_none")] @@ -716,7 +718,7 @@ mod tests { started_at: None, workspace_path: "/repo".to_string(), title: "Reasoning job".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), approval_policy: DispatchApprovalPolicy::Remote, model: Some("model-1".to_string()), reasoning_preset: Some("high".to_string()), diff --git a/src/apps/cli/src/dispatch/store.rs b/src/apps/cli/src/dispatch/store.rs index 5788e07c3e..e43094aa3b 100644 --- a/src/apps/cli/src/dispatch/store.rs +++ b/src/apps/cli/src/dispatch/store.rs @@ -2233,7 +2233,7 @@ mod tests { job_id: job_id.to_string(), session_id: format!("session-{job_id}"), workspace_path: "/tmp/workspace".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), prompt: "do the work".to_string(), approval_policy: DispatchApprovalPolicy::RejectAndReport, model: Some("model-1".to_string()), @@ -2259,7 +2259,7 @@ mod tests { project_path: Some("/tmp/workspace".to_string()), project_id: "project-1".to_string(), session_id: format!("session-{job_id}"), - agent_id: "agentic".to_string(), + agent_id: "Standard".to_string(), action: "write".to_string(), resources: vec!["src/main.rs".to_string()], save_resources: Vec::new(), diff --git a/src/apps/cli/src/main.rs b/src/apps/cli/src/main.rs index f9a4fcb393..8224e33351 100644 --- a/src/apps/cli/src/main.rs +++ b/src/apps/cli/src/main.rs @@ -168,12 +168,18 @@ struct Cli { #[arg(long)] agent: Option, - /// Select the Harness Profile for this session (minimal or balanced). - /// `minimal` is kept as a compatibility alias for agent type `minimal`. - #[arg(long, value_parser = ["minimal", "balanced"])] + /// Select the Agent Harness for this session. + #[arg(long, value_parser = harness_profile_parser())] harness_profile: Option, } +fn harness_profile_parser() -> clap::builder::PossibleValuesParser { + use openbitfun_core_types::agent_identity::HarnessId; + clap::builder::PossibleValuesParser::new(HarnessId::ALL.map(|id| { + clap::builder::PossibleValue::new(id.as_str()).aliases(id.legacy_ids().iter().copied()) + })) +} + fn shared_tui_requested(shared: bool, command: &Option) -> Result { if shared && !matches!(command, None | Some(Commands::Chat { .. })) { return Err(anyhow!( @@ -196,7 +202,7 @@ fn validate_global_harness_profile_scope( return Ok(()); } Err(anyhow!( - "--harness-profile is supported only by interactive chat and headless exec; this command cannot silently fall back to Balanced" + "--harness-profile is supported only by interactive chat and headless exec; this command cannot silently fall back to Standard" )) } @@ -208,10 +214,11 @@ fn resolved_command_harness_profile( } fn agent_type_with_harness_profile(agent_type: String, harness_profile: Option<&str>) -> String { + use openbitfun_core_types::agent_identity::canonical_agent_id; match harness_profile { - Some("minimal") => "minimal".to_string(), - Some("balanced") | None => agent_type, - Some(other) => other.to_string(), + // The retired CLI profile was an overlay: balanced preserved --agent. + Some("balanced") | None => canonical_agent_id(&agent_type).to_string(), + Some(profile) => canonical_agent_id(profile).to_string(), } } @@ -220,15 +227,15 @@ enum Commands { /// Start interactive chat (TUI) Chat { /// Agent type - #[arg(short, long, default_value = "agentic")] + #[arg(short, long, default_value = "Standard")] agent: String, /// Use the opt-in Shared Runtime for this interactive TUI #[arg(long)] shared: bool, - /// Harness Profile for this session (minimal or balanced). - #[arg(long, value_parser = ["minimal", "balanced"])] + /// Agent Harness for this session. + #[arg(long, value_parser = harness_profile_parser())] harness_profile: Option, }, @@ -246,11 +253,11 @@ enum Commands { message: Option, /// Agent type - #[arg(short, long, default_value = "agentic")] + #[arg(short, long, default_value = "Standard")] agent: String, - /// Harness Profile for this session (minimal or balanced). - #[arg(long, value_parser = ["minimal", "balanced"])] + /// Agent Harness for this session. + #[arg(long, value_parser = harness_profile_parser())] harness_profile: Option, /// Continue the most recent session in the current workspace @@ -2233,13 +2240,28 @@ mod dispatch_command_tests { #[cfg(test)] mod harness_profile_compatibility_tests { use super::{agent_type_with_harness_profile, resolved_command_harness_profile, Cli, Commands}; - use clap::Parser; + use clap::{CommandFactory, Parser}; + + #[test] + fn canonical_cli_choices_are_the_only_displayed_identifiers() { + let help = Cli::command().render_long_help().to_string(); + assert!(help.contains("Minimal, Standard, Ultimate, Creative")); + assert!(!help.contains("balanced")); + for id in openbitfun_core_types::agent_identity::HarnessId::ALL { + let cli = + Cli::try_parse_from(["openbitfun", "--harness-profile", id.as_str()]).unwrap(); + assert_eq!( + agent_type_with_harness_profile("Cowork".into(), cli.harness_profile.as_deref()), + id.as_str() + ); + } + } #[test] fn minimal_profile_is_an_agent_type_compatibility_alias() { assert_eq!( - agent_type_with_harness_profile("agentic".to_string(), Some("minimal")), - "minimal" + agent_type_with_harness_profile("Standard".to_string(), Some("minimal")), + "Minimal" ); assert_eq!( agent_type_with_harness_profile("Cowork".to_string(), Some("balanced")), diff --git a/src/apps/cli/src/modes/chat.rs b/src/apps/cli/src/modes/chat.rs index e6c0b906a1..62f0a0bb37 100644 --- a/src/apps/cli/src/modes/chat.rs +++ b/src/apps/cli/src/modes/chat.rs @@ -488,7 +488,7 @@ const LINEAGE_SETTLEMENT_RETRY_MAX: Duration = Duration::from_secs(1); pub(crate) struct ChatMode { config: CliConfig, keymap: ResolvedKeymap, - /// Current agent type (e.g. "agentic", "Cowork", "DeepResearch") + /// Current agent type (e.g. "Standard", "Cowork", "DeepResearch") agent_type: String, workspace: Option, local_cwd: std::path::PathBuf, @@ -567,7 +567,7 @@ pub(crate) struct ChatMode { /// Map agent_type to a display name for status messages fn agent_display_name(agent_type: &str) -> &'static str { match agent_type { - "agentic" => "Fang", + "Standard" => "Fang", _ => "AI Assistant", } } diff --git a/src/apps/cli/src/modes/chat/tests.rs b/src/apps/cli/src/modes/chat/tests.rs index 7d6a925dd9..2a68cdcc23 100644 --- a/src/apps/cli/src/modes/chat/tests.rs +++ b/src/apps/cli/src/modes/chat/tests.rs @@ -1425,7 +1425,7 @@ mod tests { let mut state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); state.handle_turn_started("turn", "hello"); @@ -1443,7 +1443,7 @@ mod tests { let mut state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); state.handle_turn_started("turn", "hello"); @@ -1488,7 +1488,7 @@ mod tests { let mut state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); state.handle_turn_started("turn", "/compact"); @@ -1541,7 +1541,7 @@ mod tests { let mut state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); state.handle_turn_started("turn", "/compact"); @@ -1584,7 +1584,7 @@ mod tests { let mut state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); state.handle_turn_started("turn", "/compact"); @@ -1611,7 +1611,7 @@ mod tests { let mut state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); state.current_model_id = Some("old-model-id".to_string()); @@ -1634,7 +1634,7 @@ mod tests { let mut state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); state.current_model_id = Some("removed-model".to_string()); @@ -1664,7 +1664,7 @@ mod tests { let mut state = ChatState::new( "current-session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); state.current_model_id = Some("removed-model".to_string()); @@ -1686,7 +1686,7 @@ mod tests { let mut state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); state.current_model_id = Some("newer-explicit-model".to_string()); @@ -1711,7 +1711,7 @@ mod tests { let mut state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); state.current_model_id = Some("old-model-id".to_string()); @@ -1739,7 +1739,7 @@ mod tests { let mut state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); state.current_model_id = Some("old-model-id".to_string()); @@ -1764,11 +1764,11 @@ mod tests { #[test] fn mode_selection_commits_visible_state_only_after_runtime_success() { - let mut current_mode = "agentic".to_string(); + let mut current_mode = "Standard".to_string(); let mut state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); @@ -1786,11 +1786,11 @@ mod tests { #[test] fn mode_selection_failure_preserves_visible_state_and_explains_retry() { - let mut current_mode = "agentic".to_string(); + let mut current_mode = "Standard".to_string(); let mut state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); @@ -1804,8 +1804,8 @@ mod tests { ); assert!(!applied); - assert_eq!(current_mode, "agentic"); - assert_eq!(state.agent_type, "agentic"); + assert_eq!(current_mode, "Standard"); + assert_eq!(state.agent_type, "Standard"); let notice = state.messages.last().expect("failure notice"); let crate::chat_state::FlowItem::Text { content, .. } = ¬ice.flow_items[0] else { panic!("failure notice must be text"); @@ -1842,11 +1842,11 @@ mod tests { #[test] fn unknown_mode_update_outcome_requires_restore_before_retry() { - let mut current_mode = "agentic".to_string(); + let mut current_mode = "Standard".to_string(); let mut state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); @@ -1858,8 +1858,8 @@ mod tests { ); assert!(!applied); - assert_eq!(current_mode, "agentic"); - assert_eq!(state.agent_type, "agentic"); + assert_eq!(current_mode, "Standard"); + assert_eq!(state.agent_type, "Standard"); let notice = state.messages.last().expect("unknown-outcome notice"); let crate::chat_state::FlowItem::Text { content, .. } = ¬ice.flow_items[0] else { panic!("unknown-outcome notice must be text"); @@ -1885,7 +1885,7 @@ mod tests { let mut state = ChatState::new( "session".to_string(), "Original".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); diff --git a/src/apps/cli/src/modes/chat/transcript.rs b/src/apps/cli/src/modes/chat/transcript.rs index 5c1148e526..12e15a50be 100644 --- a/src/apps/cli/src/modes/chat/transcript.rs +++ b/src/apps/cli/src/modes/chat/transcript.rs @@ -261,7 +261,7 @@ mod tests { let mut state = ChatState::new( "session-123456789".to_string(), "A session\nwith title".to_string(), - "agentic".to_string(), + "Standard".to_string(), None, ); state.messages = vec![ @@ -300,7 +300,7 @@ mod tests { let mut state = ChatState::new( "session".to_string(), "CLI Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), None, ); state.messages = vec![ diff --git a/src/apps/cli/src/peer_host/commands/session.rs b/src/apps/cli/src/peer_host/commands/session.rs index 321d07a6b5..dc4abb14d7 100644 --- a/src/apps/cli/src/peer_host/commands/session.rs +++ b/src/apps/cli/src/peer_host/commands/session.rs @@ -1035,11 +1035,11 @@ mod tests { session: AgentSessionSummary { session_id: "session_1".to_string(), session_name: "Main".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), model_id: Some("provider/model".to_string()), reasoning_preset: Some("high".to_string()), last_user_dialog_agent_type: Some("plan".to_string()), - last_submitted_agent_type: Some("agentic".to_string()), + last_submitted_agent_type: Some("Standard".to_string()), turn_count: 3, created_at_ms: 12_345, last_active_at_ms: 20_000, @@ -1049,11 +1049,11 @@ mod tests { assert_eq!(value["sessionId"], "session_1"); assert_eq!(value["sessionName"], "Main"); - assert_eq!(value["agentType"], "agentic"); + assert_eq!(value["agentType"], "Standard"); assert_eq!(value["modelName"], "provider/model"); assert_eq!(value["reasoningPreset"], "high"); assert_eq!(value["lastUserDialogAgentType"], "plan"); - assert_eq!(value["lastSubmittedAgentType"], "agentic"); + assert_eq!(value["lastSubmittedAgentType"], "Standard"); assert_eq!(value["state"], "Idle"); assert_eq!(value["turnCount"], 3); assert_eq!(value["createdAt"], 12); @@ -1073,7 +1073,7 @@ mod tests { let mut restored = CoreSession::new_with_id( "session_1".to_string(), "Main".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig::default(), ); let mut live = restored.clone(); diff --git a/src/apps/cli/src/peer_host/dispatch.rs b/src/apps/cli/src/peer_host/dispatch.rs index a05a67cfca..8e4c5368ad 100644 --- a/src/apps/cli/src/peer_host/dispatch.rs +++ b/src/apps/cli/src/peer_host/dispatch.rs @@ -1,5 +1,9 @@ //! HostInvoke / DeviceEvent dispatch for CLI Peer Host. +use openbitfun_core_types::agent_identity_wire::{ + translate_agent_identity_command, translate_agent_identity_response, AgentIdentityDialect, +}; + use serde_json::{json, Value}; use openbitfun_core::service::remote_connect::remote_server::RemoteResponse; @@ -71,8 +75,21 @@ async fn handle_host_invoke_inner(command: &str, args: Value) -> HostInvokeBridg Err(e) => return HostInvokeBridgeResult::err(e), }; + let args = + match translate_agent_identity_command(command, args, AgentIdentityDialect::Canonical) { + Ok(args) => args, + Err(error) => return HostInvokeBridgeResult::err(error), + }; match commands::dispatch(command, &args, state).await { - Ok(value) => HostInvokeBridgeResult::ok_value(value), + Ok(value) => match translate_agent_identity_response( + command, + value, + &args, + AgentIdentityDialect::Legacy, + ) { + Ok(value) => HostInvokeBridgeResult::ok_value(value), + Err(error) => HostInvokeBridgeResult::err(error), + }, Err(error) => HostInvokeBridgeResult::err(error), } } diff --git a/src/apps/cli/src/peer_host/fanout.rs b/src/apps/cli/src/peer_host/fanout.rs index 6562849db3..f6eca80a93 100644 --- a/src/apps/cli/src/peer_host/fanout.rs +++ b/src/apps/cli/src/peer_host/fanout.rs @@ -657,6 +657,15 @@ async fn fanout_peer_device_event_once(queued: QueuedPeerDeviceEvent) { continuity, terminal, } = queued; + let mut payload = payload; + if let Err(error) = openbitfun_core_types::agent_identity_wire::translate_agent_identity_fields( + &mut payload, + openbitfun_core_types::agent_identity_wire::AgentIdentityDialect::Legacy, + ) { + tracing::warn!( + "Peer event contains conflicting Agent profiles; preserving records: {error}" + ); + } if !continuity_is_current(&continuity) { return; } diff --git a/src/apps/cli/src/shared_runtime.rs b/src/apps/cli/src/shared_runtime.rs index 81adf46e16..6ca7ab653f 100644 --- a/src/apps/cli/src/shared_runtime.rs +++ b/src/apps/cli/src/shared_runtime.rs @@ -1480,7 +1480,7 @@ mod tests { &self, _session_id: &str, ) -> PortResult> { - Ok(Some("agentic".to_string())) + Ok(Some("Standard".to_string())) } } @@ -1701,7 +1701,7 @@ mod tests { project_path: None, project_id: "project-1".to_string(), session_id: session_id.to_string(), - agent_id: "agentic".to_string(), + agent_id: "Standard".to_string(), action: "run command".to_string(), resources: Vec::new(), save_resources: Vec::new(), diff --git a/src/apps/cli/src/ui/agent_selector.rs b/src/apps/cli/src/ui/agent_selector.rs index bfabc72ce3..2d1001c4a5 100644 --- a/src/apps/cli/src/ui/agent_selector.rs +++ b/src/apps/cli/src/ui/agent_selector.rs @@ -381,7 +381,7 @@ mod tests { fn modes() -> Vec { vec![ AgentItem { - id: "agentic".to_string(), + id: "Standard".to_string(), route_key: None, description: "General purpose".to_string(), }, @@ -396,11 +396,11 @@ mod tests { #[test] fn chat_agent_entry_keeps_modes_direct_and_adds_management_rows() { let mut state = AgentSelectorState::new(); - state.show(modes(), Some("agentic".to_string()), true, true); + state.show(modes(), Some("Standard".to_string()), true, true); assert!(matches!( state.confirm_selection(), - Some(AgentSelectorAction::SwitchMode(AgentItem { id, .. })) if id == "agentic" + Some(AgentSelectorAction::SwitchMode(AgentItem { id, .. })) if id == "Standard" )); state.move_down(); state.move_down(); @@ -418,7 +418,7 @@ mod tests { #[test] fn startup_agent_entry_omits_session_scoped_external_sources() { let mut state = AgentSelectorState::new(); - state.show(modes(), Some("agentic".to_string()), false, true); + state.show(modes(), Some("Standard".to_string()), false, true); state.move_down(); state.move_down(); assert!(matches!( @@ -428,14 +428,14 @@ mod tests { state.move_down(); assert!(matches!( state.confirm_selection(), - Some(AgentSelectorAction::SwitchMode(AgentItem { id, .. })) if id == "agentic" + Some(AgentSelectorAction::SwitchMode(AgentItem { id, .. })) if id == "Standard" )); } #[test] fn mode_only_entry_exposes_no_management_rows() { let mut state = AgentSelectorState::new(); - state.show_modes_only(modes(), Some("agentic".to_string()), true); + state.show_modes_only(modes(), Some("Standard".to_string()), true); state.move_down(); assert!(matches!( @@ -445,14 +445,14 @@ mod tests { state.move_down(); assert!(matches!( state.confirm_selection(), - Some(AgentSelectorAction::SwitchMode(AgentItem { id, .. })) if id == "agentic" + Some(AgentSelectorAction::SwitchMode(AgentItem { id, .. })) if id == "Standard" )); } #[test] fn mode_only_entry_renders_no_management_copy() { let mut state = AgentSelectorState::new(); - state.show_modes_only(modes(), Some("agentic".to_string()), true); + state.show_modes_only(modes(), Some("Standard".to_string()), true); let mut terminal = Terminal::new(TestBackend::new(52, 12)).expect("test terminal"); terminal @@ -477,11 +477,11 @@ mod tests { #[test] fn processing_turn_keeps_management_available_and_defers_mode_guard_to_dispatch() { let mut state = AgentSelectorState::new(); - state.show(modes(), Some("agentic".to_string()), true, false); + state.show(modes(), Some("Standard".to_string()), true, false); assert!(matches!( state.confirm_selection(), - Some(AgentSelectorAction::SwitchMode(AgentItem { id, .. })) if id == "agentic" + Some(AgentSelectorAction::SwitchMode(AgentItem { id, .. })) if id == "Standard" )); state.move_down(); state.move_down(); @@ -494,7 +494,7 @@ mod tests { #[test] fn narrow_processing_popup_keeps_mode_unavailability_reason_visible() { let mut state = AgentSelectorState::new(); - state.show(modes(), Some("agentic".to_string()), true, false); + state.show(modes(), Some("Standard".to_string()), true, false); let mut terminal = Terminal::new(TestBackend::new(32, 9)).expect("test terminal"); terminal @@ -523,14 +523,14 @@ mod tests { #[test] fn open_popup_refreshes_mode_availability_when_the_turn_ends() { let mut state = AgentSelectorState::new(); - state.show(modes(), Some("agentic".to_string()), true, false); + state.show(modes(), Some("Standard".to_string()), true, false); state.set_mode_switch_allowed(true); assert!(state.allow_mode_switch); assert!(matches!( state.confirm_selection(), - Some(AgentSelectorAction::SwitchMode(AgentItem { id, .. })) if id == "agentic" + Some(AgentSelectorAction::SwitchMode(AgentItem { id, .. })) if id == "Standard" )); } @@ -554,7 +554,7 @@ mod tests { #[test] fn too_small_fallback_disables_hidden_selection() { let mut state = AgentSelectorState::new(); - state.show(modes(), Some("agentic".to_string()), true, true); + state.show(modes(), Some("Standard".to_string()), true, true); let mut terminal = Terminal::new(TestBackend::new(10, 3)).expect("test terminal"); terminal .draw(|frame| state.render(frame, frame.area(), &Theme::dark_ansi16())) diff --git a/src/apps/cli/src/ui/chat/popups.rs b/src/apps/cli/src/ui/chat/popups.rs index c23c03d9d3..2452b966bc 100644 --- a/src/apps/cli/src/ui/chat/popups.rs +++ b/src/apps/cli/src/ui/chat/popups.rs @@ -796,11 +796,11 @@ mod tests { let mut view = ChatView::new(Theme::dark(), Vec::new()); view.show_agent_selector( vec![AgentItem { - id: "agentic".to_string(), + id: "Standard".to_string(), route_key: None, description: "General purpose".to_string(), }], - Some("agentic".to_string()), + Some("Standard".to_string()), true, true, ); @@ -816,11 +816,11 @@ mod tests { let mut view = ChatView::new(Theme::dark(), Vec::new()); view.show_agent_modes_only( vec![AgentItem { - id: "agentic".to_string(), + id: "Standard".to_string(), route_key: None, description: "General purpose".to_string(), }], - Some("agentic".to_string()), + Some("Standard".to_string()), true, ); @@ -829,7 +829,7 @@ mod tests { let state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("D:/workspace/current".to_string()), ); let mut terminal = Terminal::new(TestBackend::new(80, 20)).expect("test terminal"); diff --git a/src/apps/cli/src/ui/chat/scroll.rs b/src/apps/cli/src/ui/chat/scroll.rs index 3e07dc5663..f5b531f865 100644 --- a/src/apps/cli/src/ui/chat/scroll.rs +++ b/src/apps/cli/src/ui/chat/scroll.rs @@ -299,7 +299,7 @@ mod transcript_navigation_tests { let mut chat_state = ChatState::new( "session".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), None, ); chat_state.messages = vec![ diff --git a/src/apps/cli/src/ui/chat/status.rs b/src/apps/cli/src/ui/chat/status.rs index 2a889f31f1..cd5fc779fa 100644 --- a/src/apps/cli/src/ui/chat/status.rs +++ b/src/apps/cli/src/ui/chat/status.rs @@ -201,7 +201,7 @@ mod status_tests { let mut state = ChatState::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), None, ); state.metadata.message_count = 3; @@ -224,7 +224,7 @@ mod status_tests { let mut state = ChatState::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), Some("/tmp/project".to_string()), ); state.current_model_name = "Example Model".to_string(); @@ -239,7 +239,7 @@ mod status_tests { "ID: session-1", "Runtime: Embedded TUI", "State: Idle", - "Agent: agentic", + "Agent: Standard", "Model: Example Model", "Reasoning: Auto", "Approval: Ask", @@ -269,7 +269,7 @@ mod status_tests { let mut state = ChatState::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), None, ); state.is_processing = true; diff --git a/src/apps/cli/src/ui/permission.rs b/src/apps/cli/src/ui/permission.rs index 5686b75d63..d7e231a859 100644 --- a/src/apps/cli/src/ui/permission.rs +++ b/src/apps/cli/src/ui/permission.rs @@ -300,7 +300,7 @@ mod tests { project_path: None, project_id: "project-1".to_string(), session_id: "session-1".to_string(), - agent_id: "agentic".to_string(), + agent_id: "Standard".to_string(), action: "edit".to_string(), resources: vec!["src/main.rs".to_string()], save_resources: vec!["src/main.rs".to_string()], diff --git a/src/apps/cli/src/ui/startup.rs b/src/apps/cli/src/ui/startup.rs index 391f01a607..084918c377 100644 --- a/src/apps/cli/src/ui/startup.rs +++ b/src/apps/cli/src/ui/startup.rs @@ -2501,8 +2501,8 @@ mod logo_contract_tests { #[test] fn external_or_unknown_startup_modes_do_not_change_the_shared_default() { let local = TuiAgentMode { - id: "agentic".to_string(), - route_key: "agentic".to_string(), + id: "Standard".to_string(), + route_key: "Standard".to_string(), description: String::new(), model_id: None, is_external: false, diff --git a/src/apps/cli/tests/acp_stdio_cli.rs b/src/apps/cli/tests/acp_stdio_cli.rs index 83d07553c4..0793c4afc0 100644 --- a/src/apps/cli/tests/acp_stdio_cli.rs +++ b/src/apps/cli/tests/acp_stdio_cli.rs @@ -433,7 +433,7 @@ async fn acp_stdio_preserves_mode_and_history_across_restart_then_closes_active_ ); assert_eq!( current_config_value(&loaded_with_fallback, "mode"), - Some(&json!("agentic")), + Some(&json!("Standard")), "an unavailable persisted mode must be migrated to an executable fallback" ); let (fallback_prompt, _) = second diff --git a/src/apps/desktop/src/api/agentic_api.rs b/src/apps/desktop/src/api/agentic_api.rs index 9e4872c627..7d8e9a85ac 100644 --- a/src/apps/desktop/src/api/agentic_api.rs +++ b/src/apps/desktop/src/api/agentic_api.rs @@ -4256,7 +4256,7 @@ mod tests { "sessionId": "session-1", "userInput": "resolved input", "originalUserInput": "original input", - "agentType": "agentic", + "agentType": "Standard", "workspacePath": "/worktrees/session-1", "projectWorkspacePath": "/workspace/project", "remoteConnectionId": "connection-1", @@ -4289,7 +4289,7 @@ mod tests { Some("original input") ); assert_eq!(runtime_request.turn_id.as_deref(), Some("turn-1")); - assert_eq!(runtime_request.agent_type, "agentic"); + assert_eq!(runtime_request.agent_type, "Standard"); assert_eq!( runtime_request.workspace_path.as_deref(), Some("/workspace/project") @@ -4389,7 +4389,7 @@ mod tests { let request = serde_json::from_value::(json!({ "sessionId": "session-1", "userInput": "hello", - "agentType": "agentic", + "agentType": "Standard", "userMessageMetadata": "not-an-object" })) .expect("legacy metadata request"); @@ -4664,7 +4664,7 @@ mod tests { let mut session = Session::new_with_id( "session-1".to_string(), "Tail view".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig::default(), ); session.dialog_turn_ids = vec!["turn-49".to_string(), "turn-50".to_string()]; diff --git a/src/apps/desktop/src/api/dispatch_api.rs b/src/apps/desktop/src/api/dispatch_api.rs index cea17b1bc3..2cfddad774 100644 --- a/src/apps/desktop/src/api/dispatch_api.rs +++ b/src/apps/desktop/src/api/dispatch_api.rs @@ -38,6 +38,12 @@ struct AccountDeviceDispatchRpc; #[async_trait] impl DeviceDispatchRpc for AccountDeviceDispatchRpc { async fn invoke(&self, device_id: &str, command: &str, args: Value) -> anyhow::Result { + use openbitfun_core_types::agent_identity_wire::{ + translate_agent_identity_command, translate_agent_identity_response, + AgentIdentityDialect, + }; + let args = translate_agent_identity_command(command, args, AgentIdentityDialect::Legacy) + .map_err(anyhow::Error::msg)?; let command_json = serde_json::to_string(&serde_json::json!({ "cmd": "host_invoke", "command": command, @@ -50,7 +56,13 @@ impl DeviceDispatchRpc for AccountDeviceDispatchRpc { ) .await .map_err(anyhow::Error::msg)?; - decode_device_dispatch_rpc(&raw) + translate_agent_identity_response( + command, + decode_device_dispatch_rpc(&raw)?, + &Value::Null, + AgentIdentityDialect::Canonical, + ) + .map_err(anyhow::Error::msg) } } diff --git a/src/apps/desktop/src/api/peer_host_invoke.rs b/src/apps/desktop/src/api/peer_host_invoke.rs index 8e7e931cfa..423b1d0ead 100644 --- a/src/apps/desktop/src/api/peer_host_invoke.rs +++ b/src/apps/desktop/src/api/peer_host_invoke.rs @@ -322,8 +322,40 @@ pub async fn dispatch(command: &str, args: Value) -> HostInvokeBridgeResult { } }; + use openbitfun_core_types::agent_identity_wire::{ + translate_agent_identity_command, translate_agent_identity_response, AgentIdentityDialect, + }; + let args = + match translate_agent_identity_command(command, args, AgentIdentityDialect::Canonical) { + Ok(args) => args, + Err(error) => { + return HostInvokeBridgeResult { + ok: false, + value: None, + error: Some(error), + } + } + }; + let response_scope = + serde_json::json!({"path": args.get("request").unwrap_or(&args).get("path")}); match bridge_via_webview(&app, command, args).await { - Ok(result) => result, + Ok(mut result) => { + if let Some(value) = result.value.take() { + match translate_agent_identity_response( + command, + value, + &response_scope, + AgentIdentityDialect::Legacy, + ) { + Ok(value) => result.value = Some(value), + Err(error) => { + result.ok = false; + result.error = Some(error); + } + } + } + result + } Err(error) => HostInvokeBridgeResult { ok: false, value: None, diff --git a/src/apps/desktop/src/api/remote_connect_api.rs b/src/apps/desktop/src/api/remote_connect_api.rs index bcf286dd90..c74624f450 100644 --- a/src/apps/desktop/src/api/remote_connect_api.rs +++ b/src/apps/desktop/src/api/remote_connect_api.rs @@ -369,9 +369,16 @@ async fn fanout_peer_device_event_once(item: PeerEventFanoutItem) { return; } use openbitfun_core::service::remote_connect::remote_server::RemoteCommand; + let mut payload = item.payload; + if let Err(error) = openbitfun_core_types::agent_identity_wire::translate_agent_identity_fields( + &mut payload, + openbitfun_core_types::agent_identity_wire::AgentIdentityDialect::Legacy, + ) { + log::warn!("Peer event contains conflicting Agent profiles; preserving records: {error}"); + } let envelope = match serde_json::to_string(&RemoteCommand::DeviceEvent { event: item.event.clone(), - payload: item.payload, + payload, }) { Ok(s) => s, Err(e) => { @@ -2523,7 +2530,7 @@ pub async fn account_connect_devices() -> Result, String> } }; let agent = - agent_type.unwrap_or_else(|| "agentic".to_string()); + agent_type.unwrap_or_else(|| "Standard".to_string()); if let Err(e) = scheduler .submit( session_id, diff --git a/src/apps/desktop/src/runtime/session_application.rs b/src/apps/desktop/src/runtime/session_application.rs index f5b0a7caa6..00dc589f2b 100644 --- a/src/apps/desktop/src/runtime/session_application.rs +++ b/src/apps/desktop/src/runtime/session_application.rs @@ -1232,7 +1232,7 @@ mod tests { let mut restored = Session::new_with_id( "session-1".to_string(), "Restored".to_string(), - "agentic".to_string(), + "Standard".to_string(), Default::default(), ); let mut live = restored.clone(); @@ -1257,7 +1257,7 @@ mod tests { let mut restored = Session::new_with_id( "session-1".to_string(), "Restored".to_string(), - "agentic".to_string(), + "Standard".to_string(), Default::default(), ); @@ -1424,7 +1424,7 @@ mod tests { let mut incoming = current.clone(); incoming.session_name = "Renamed".to_string(); - incoming.agent_type = "agentic".to_string(); + incoming.agent_type = "Standard".to_string(); incoming.model_name = "stale-model".to_string(); incoming.memory_mode = SessionMemoryMode::Enabled; incoming.status = SessionStatus::Active; @@ -1487,7 +1487,7 @@ mod tests { let mut current = SessionMetadata::new( "session".to_string(), "Current".to_string(), - "agentic".to_string(), + "Standard".to_string(), "primary".to_string(), ); current.review_action_state = Some(json!({ "phase": "review_completed" })); diff --git a/src/apps/sdk-host/tests/stdio_transport.rs b/src/apps/sdk-host/tests/stdio_transport.rs index 74aa3e9e94..2a172b572b 100644 --- a/src/apps/sdk-host/tests/stdio_transport.rs +++ b/src/apps/sdk-host/tests/stdio_transport.rs @@ -106,7 +106,7 @@ impl AgentSubmissionPort for MinimalOwner { } async fn resolve_session_agent_type(&self, _session_id: &str) -> PortResult> { - Ok(Some("agentic".to_string())) + Ok(Some("Standard".to_string())) } } @@ -169,7 +169,7 @@ impl AgentSubmissionPort for BlockingCreateOwner { } async fn resolve_session_agent_type(&self, _session_id: &str) -> PortResult> { - Ok(Some("agentic".to_string())) + Ok(Some("Standard".to_string())) } } diff --git a/src/crates/adapters/agent-runtime-ipc/src/tests/protocol_contracts.rs b/src/crates/adapters/agent-runtime-ipc/src/tests/protocol_contracts.rs index 878c3a1bec..b5c73f5207 100644 --- a/src/crates/adapters/agent-runtime-ipc/src/tests/protocol_contracts.rs +++ b/src/crates/adapters/agent-runtime-ipc/src/tests/protocol_contracts.rs @@ -592,7 +592,7 @@ fn submit_turn_accepts_the_existing_64_kib_tui_paste_contract() { original_message: None, turn_id: Some("turn-1".to_string()), execution: Default::default(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some("D:/workspace/project".to_string()), remote_connection_id: None, remote_ssh_host: None, diff --git a/src/crates/adapters/agent-runtime-ipc/src/tests/shared_controller.rs b/src/crates/adapters/agent-runtime-ipc/src/tests/shared_controller.rs index 5b316d166c..d5c0343075 100644 --- a/src/crates/adapters/agent-runtime-ipc/src/tests/shared_controller.rs +++ b/src/crates/adapters/agent-runtime-ipc/src/tests/shared_controller.rs @@ -148,7 +148,7 @@ impl RuntimeIpcRequestHandler for CreateRaceHandler { self.create_started.notify_one(); self.allow_create.notified().await; let mut session = - AgentSessionCreateResult::new("session-a", "Created session", "agentic"); + AgentSessionCreateResult::new("session-a", "Created session", "Standard"); session.workspace_path = request.workspace_path; session.workspace_id = Some("workspace-fixture".to_string()); Ok(RuntimeIpcOperationResult::SessionCreated { session }) @@ -610,7 +610,7 @@ fn summary(session_id: &str) -> AgentSessionSummary { AgentSessionSummary { session_id: session_id.to_string(), session_name: "Shared session".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), model_id: None, reasoning_preset: None, last_user_dialog_agent_type: None, @@ -669,7 +669,7 @@ fn create_operation(workspace: &Path, name: &str) -> RuntimeIpcOperation { RuntimeIpcOperation::CreateSession { request: AgentSessionCreateRequest { session_name: name.to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), agent_route_key: None, workspace_path: Some(workspace.to_string_lossy().to_string()), project_workspace_path: None, @@ -692,7 +692,7 @@ fn submit_operation(workspace: &Path, session_id: &str, turn_id: &str) -> Runtim original_message: None, turn_id: Some(turn_id.to_string()), execution: Default::default(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some(workspace.to_string_lossy().to_string()), remote_connection_id: None, remote_ssh_host: None, @@ -1274,7 +1274,7 @@ async fn mode_update_requires_the_controlled_idle_session() { expect_error( &mut client, 7, - update_mode_operation("session-a", "agentic"), + update_mode_operation("session-a", "Standard"), RuntimeIpcErrorCode::SessionInUse, ) .await; diff --git a/src/crates/assembly/agent-content/prompts/agents/claw_mode.md b/src/crates/assembly/agent-content/prompts/agents/claw_mode.md index 9e943207e1..d081f5dfcb 100644 --- a/src/crates/assembly/agent-content/prompts/agents/claw_mode.md +++ b/src/crates/assembly/agent-content/prompts/agents/claw_mode.md @@ -45,7 +45,7 @@ Use this pattern when: Choose the session type intentionally: -- `agentic` for implementation, debugging, code changes, and planning tasks; ask it to use the built-in `plan` Skill when a plan artifact is the deliverable. +- `Standard` for implementation, debugging, code changes, and planning tasks; ask it to use the built-in `plan` Skill when a plan artifact is the deliverable. - `Cowork` for research, documents, presentations, summaries, and other office-related work. Local computer/desktop work is not a SessionControl session type; delegate it through `Task` with the `ComputerUse` sub-agent when that subagent type is available. diff --git a/src/crates/assembly/agent-content/prompts/agents/ultra_mode.md b/src/crates/assembly/agent-content/prompts/agents/ultra_mode.md index 6d9119c169..eb79f996aa 100644 --- a/src/crates/assembly/agent-content/prompts/agents/ultra_mode.md +++ b/src/crates/assembly/agent-content/prompts/agents/ultra_mode.md @@ -1,6 +1,6 @@ # Role -You are OpenBitFun in Ultra mode, the root planner for a bounded Swarm of collaborating agents. +You are OpenBitFun using the Ultimate Harness, the root planner for a bounded Swarm of collaborating agents. Your responsibilities are to: @@ -124,4 +124,4 @@ IMPORTANT: Whenever you mention a file path in normal prose that the user might - Absolute path as plain text: /external/project/deep-research/report.md -{LANGUAGE_PREFERENCE} \ No newline at end of file +{LANGUAGE_PREFERENCE} diff --git a/src/crates/assembly/agent-content/prompts/insights/suggestions.md b/src/crates/assembly/agent-content/prompts/insights/suggestions.md index 214c8efe02..80da2ca608 100644 --- a/src/crates/assembly/agent-content/prompts/insights/suggestions.md +++ b/src/crates/assembly/agent-content/prompts/insights/suggestions.md @@ -49,16 +49,16 @@ Analyze this OpenBitFun usage data and suggest improvements. } ``` -4. **Agents and Skills**: Switch between Agentic and Cowork for different tasks, and use skills such as Plan for task-specific workflows. +4. **Agents and Skills**: Switch between Standard and Cowork for different tasks, and use skills such as Plan for task-specific workflows. - How to use: Select the appropriate mode from the mode switcher based on your task. - Mode comparison: | Mode | Best for | AI behavior | |------|----------|-------------| - | **Agentic** | Autonomous implementation | AI plans and executes independently | + | **Standard** | Autonomous implementation | AI plans and executes independently | | **Cowork** | Collaborative editing | AI suggests, you approve each change | | **Plan** | Architecture & design | AI creates detailed plans before coding | - For work with independent branches, enable or invoke the built-in `multitask` skill so the Agent can coordinate parallel subagents without changing the session's main Agent. - - Tip: Use the Plan skill for complex tasks, then ask Agentic to implement the resulting plan. + - Tip: Use the Plan skill for complex tasks, then ask Standard to implement the resulting plan. 5. **CLI Exec (Headless)**: Run OpenBitFun non-interactively from scripts and CI/CD pipelines. - How to use: `openbitfun exec "fix lint errors" --tools "Edit,Read,ExecCommand"` diff --git a/src/crates/assembly/core/src/agentic/agents/definitions/modes/creative.rs b/src/crates/assembly/core/src/agentic/agents/definitions/modes/creative.rs index 7e4ac1f20f..8c4885bc63 100644 --- a/src/crates/assembly/core/src/agentic/agents/definitions/modes/creative.rs +++ b/src/crates/assembly/core/src/agentic/agents/definitions/modes/creative.rs @@ -1,28 +1,28 @@ -//! Creative Mode +//! Creative Harness //! //! Owns product-creation capabilities that intentionally do not appear in the //! default tool manifests of general coding, office, or assistant modes. use crate::agentic::agents::{ - shared_coding_mode_tool_exposure_overrides, shared_coding_mode_tools, - shared_coding_mode_user_context_policy, Agent, AgentToolPolicyOverrides, UserContextPolicy, + standard_harness_tool_exposure_overrides, standard_harness_tools, + standard_harness_user_context_policy, Agent, AgentToolPolicyOverrides, UserContextPolicy, }; use async_trait::async_trait; -pub struct CreativeMode { +pub struct CreativeHarness { default_tools: Vec, tool_exposure_overrides: AgentToolPolicyOverrides, } -impl Default for CreativeMode { +impl Default for CreativeHarness { fn default() -> Self { Self::new() } } -impl CreativeMode { +impl CreativeHarness { pub fn new() -> Self { - let mut default_tools = shared_coding_mode_tools(); + let mut default_tools = standard_harness_tools(); default_tools.extend( [ "InitMiniApp", @@ -35,13 +35,13 @@ impl CreativeMode { ); Self { default_tools, - tool_exposure_overrides: shared_coding_mode_tool_exposure_overrides(), + tool_exposure_overrides: standard_harness_tool_exposure_overrides(), } } } #[async_trait] -impl Agent for CreativeMode { +impl Agent for CreativeHarness { fn as_any(&self) -> &dyn std::any::Any { self } @@ -55,7 +55,7 @@ impl Agent for CreativeMode { } fn description(&self) -> &str { - "Creation mode for building MiniApps and safely customizing the running OpenBitFun frontend" + "Creative Harness for building MiniApps and customizing the OpenBitFun interface" } fn prompt_template_name(&self, _model_name: Option<&str>) -> &str { @@ -71,7 +71,7 @@ impl Agent for CreativeMode { } fn user_context_policy(&self) -> UserContextPolicy { - shared_coding_mode_user_context_policy() + standard_harness_user_context_policy() } fn is_readonly(&self) -> bool { @@ -81,12 +81,12 @@ impl Agent for CreativeMode { #[cfg(test)] mod tests { - use super::CreativeMode; + use super::CreativeHarness; use crate::agentic::agents::Agent; #[test] fn creative_mode_owns_product_creation_tools() { - let tools = CreativeMode::new().default_tools(); + let tools = CreativeHarness::new().default_tools(); for tool in [ "InitMiniApp", "FinalizeMiniApp", @@ -99,7 +99,7 @@ mod tests { #[test] fn creative_prompt_has_its_own_persistent_cache_identity() { - let mode = CreativeMode::new(); + let mode = CreativeHarness::new(); assert_eq!(mode.prompt_template_name(None), "creative_mode"); let prompt = crate::agentic::agents::get_embedded_prompt("creative_mode").unwrap(); assert!(prompt.contains("OpenBitFunControl")); diff --git a/src/crates/assembly/core/src/agentic/agents/definitions/modes/minimal.rs b/src/crates/assembly/core/src/agentic/agents/definitions/modes/minimal.rs index added66978..195d2d3fe9 100644 --- a/src/crates/assembly/core/src/agentic/agents/definitions/modes/minimal.rs +++ b/src/crates/assembly/core/src/agentic/agents/definitions/modes/minimal.rs @@ -2,17 +2,17 @@ use crate::agentic::agents::{Agent, UserContextPolicy}; use async_trait::async_trait; /// A focused Agent with a stable prompt and tool manifest. -pub struct MinimalMode { +pub struct MinimalHarness { default_tools: Vec, } -impl Default for MinimalMode { +impl Default for MinimalHarness { fn default() -> Self { Self::new() } } -impl MinimalMode { +impl MinimalHarness { pub fn new() -> Self { let default_tools = [ "Read", @@ -30,13 +30,13 @@ impl MinimalMode { } #[async_trait] -impl Agent for MinimalMode { +impl Agent for MinimalHarness { fn as_any(&self) -> &dyn std::any::Any { self } fn id(&self) -> &str { - "minimal" + "Minimal" } fn name(&self) -> &str { @@ -44,7 +44,7 @@ impl Agent for MinimalMode { } fn description(&self) -> &str { - "Minimal coding mode with a stable, focused tool set." + "Minimal Harness for coding with a stable, focused tool set." } fn prompt_template_name(&self, _model_name: Option<&str>) -> &str { @@ -72,12 +72,12 @@ impl Agent for MinimalMode { #[cfg(test)] mod tests { - use super::MinimalMode; + use super::MinimalHarness; use crate::agentic::agents::Agent; #[test] fn minimal_manifest_is_stable_and_has_no_listing_tools() { - let mode = MinimalMode::new(); + let mode = MinimalHarness::new(); assert_eq!( mode.default_tools(), [ diff --git a/src/crates/assembly/core/src/agentic/agents/definitions/modes/mod.rs b/src/crates/assembly/core/src/agentic/agents/definitions/modes/mod.rs index eb736d6916..bf8e47734f 100644 --- a/src/crates/assembly/core/src/agentic/agents/definitions/modes/mod.rs +++ b/src/crates/assembly/core/src/agentic/agents/definitions/modes/mod.rs @@ -1,15 +1,15 @@ -mod agentic; mod claw; mod cowork; mod creative; mod deep_research; mod minimal; -mod ultra; +mod standard; +mod ultimate; -pub use agentic::AgenticMode; pub use claw::ClawMode; pub use cowork::CoworkMode; -pub use creative::CreativeMode; +pub use creative::CreativeHarness; pub use deep_research::DeepResearchMode; -pub use minimal::MinimalMode; -pub use ultra::UltraMode; +pub use minimal::MinimalHarness; +pub use standard::StandardHarness; +pub use ultimate::UltimateHarness; diff --git a/src/crates/assembly/core/src/agentic/agents/definitions/modes/agentic.rs b/src/crates/assembly/core/src/agentic/agents/definitions/modes/standard.rs similarity index 57% rename from src/crates/assembly/core/src/agentic/agents/definitions/modes/agentic.rs rename to src/crates/assembly/core/src/agentic/agents/definitions/modes/standard.rs index c5f9bf4bfd..2a6fb14c65 100644 --- a/src/crates/assembly/core/src/agentic/agents/definitions/modes/agentic.rs +++ b/src/crates/assembly/core/src/agentic/agents/definitions/modes/standard.rs @@ -1,54 +1,54 @@ -//! Agentic Mode +//! Standard Harness //! //! Uses the shared coding prompt, tools, exposure policy, and user context. use crate::agentic::agents::{ - shared_coding_mode_tool_exposure_overrides, shared_coding_mode_tools, - shared_coding_mode_user_context_policy, Agent, AgentToolPolicyOverrides, UserContextPolicy, - SHARED_CODING_MODE_PROMPT_TEMPLATE, + standard_harness_tool_exposure_overrides, standard_harness_tools, + standard_harness_user_context_policy, Agent, AgentToolPolicyOverrides, UserContextPolicy, + STANDARD_HARNESS_PROMPT_TEMPLATE, }; use async_trait::async_trait; -pub struct AgenticMode { +pub struct StandardHarness { default_tools: Vec, tool_exposure_overrides: AgentToolPolicyOverrides, } -impl Default for AgenticMode { +impl Default for StandardHarness { fn default() -> Self { Self::new() } } -impl AgenticMode { +impl StandardHarness { pub fn new() -> Self { Self { - default_tools: shared_coding_mode_tools(), - tool_exposure_overrides: shared_coding_mode_tool_exposure_overrides(), + default_tools: standard_harness_tools(), + tool_exposure_overrides: standard_harness_tool_exposure_overrides(), } } } #[async_trait] -impl Agent for AgenticMode { +impl Agent for StandardHarness { fn as_any(&self) -> &dyn std::any::Any { self } fn id(&self) -> &str { - "agentic" + "Standard" } fn name(&self) -> &str { - "Agentic" + "Standard" } fn description(&self) -> &str { - "Full-featured AI assistant with access to all tools for comprehensive software development tasks" + "Standard Harness for software development with task planning, tools, and delegated subagents" } fn prompt_template_name(&self, _model_name: Option<&str>) -> &str { - SHARED_CODING_MODE_PROMPT_TEMPLATE + STANDARD_HARNESS_PROMPT_TEMPLATE } fn default_tools(&self) -> Vec { @@ -60,7 +60,7 @@ impl Agent for AgenticMode { } fn user_context_policy(&self) -> UserContextPolicy { - shared_coding_mode_user_context_policy() + standard_harness_user_context_policy() } fn is_readonly(&self) -> bool { diff --git a/src/crates/assembly/core/src/agentic/agents/definitions/modes/ultra.rs b/src/crates/assembly/core/src/agentic/agents/definitions/modes/ultimate.rs similarity index 81% rename from src/crates/assembly/core/src/agentic/agents/definitions/modes/ultra.rs rename to src/crates/assembly/core/src/agentic/agents/definitions/modes/ultimate.rs index 822fa356df..9c4cc9bd3e 100644 --- a/src/crates/assembly/core/src/agentic/agents/definitions/modes/ultra.rs +++ b/src/crates/assembly/core/src/agentic/agents/definitions/modes/ultimate.rs @@ -2,17 +2,17 @@ use crate::agentic::agents::{Agent, UserContextPolicy}; use async_trait::async_trait; /// The user-facing entry point for the Swarm planner. -pub struct UltraMode { +pub struct UltimateHarness { default_tools: Vec, } -impl Default for UltraMode { +impl Default for UltimateHarness { fn default() -> Self { Self::new() } } -impl UltraMode { +impl UltimateHarness { pub fn new() -> Self { Self { default_tools: [ @@ -43,18 +43,18 @@ impl UltraMode { } #[async_trait] -impl Agent for UltraMode { +impl Agent for UltimateHarness { fn as_any(&self) -> &dyn std::any::Any { self } fn id(&self) -> &str { - "Ultra" + "Ultimate" } fn name(&self) -> &str { - "Ultra" + "Ultimate" } fn description(&self) -> &str { - "Swarm planning mode for decomposing complex work into coordinated worker and review tasks. It may issue many model requests concurrently, increasing API cost and provider rate-limit pressure." + "Ultimate Harness for decomposing complex work into coordinated worker and review tasks. It may issue many model requests concurrently, increasing API cost and provider rate-limit pressure." } fn prompt_template_name(&self, _model_name: Option<&str>) -> &str { "ultra_mode" diff --git a/src/crates/assembly/core/src/agentic/agents/mod.rs b/src/crates/assembly/core/src/agentic/agents/mod.rs index a0ba048328..15f648461f 100644 --- a/src/crates/assembly/core/src/agentic/agents/mod.rs +++ b/src/crates/assembly/core/src/agentic/agents/mod.rs @@ -16,7 +16,8 @@ pub use definitions::custom::{CustomMode, CustomSubagent, CustomSubagentKind}; pub(crate) use definitions::external::ExternalProvidedAgent; pub use definitions::hidden::{CodeReviewAgent, DeepReviewAgent, GenerateDocAgent}; pub use definitions::modes::{ - AgenticMode, ClawMode, CoworkMode, CreativeMode, DeepResearchMode, MinimalMode, UltraMode, + ClawMode, CoworkMode, CreativeHarness, DeepResearchMode, MinimalHarness, StandardHarness, + UltimateHarness, }; pub use definitions::review::{ReviewFixerAgent, ReviewJudgeAgent, ReviewWorkerAgent}; pub use definitions::shared::ReadonlySubagent; @@ -28,9 +29,9 @@ use indexmap::IndexMap; pub use openbitfun_agent_runtime::agents::{ is_swarm_delegate_agent_type, is_swarm_planner_agent_type, mode_config_profile_label, mode_config_profile_member_mode_ids, mode_presentation_rank, resolve_mode_config_profile_id, - shared_coding_mode_user_context_policy, SHARED_CODING_MODE_CONFIG_PROFILE_ID, - SHARED_CODING_MODE_CONFIG_PROFILE_LABEL, SHARED_CODING_MODE_IDS, - SHARED_CODING_MODE_PROMPT_TEMPLATE, SWARM_DELEGATE_AGENT_TYPES, SWARM_PLANNER_AGENT_TYPES, + standard_harness_user_context_policy, STANDARD_HARNESS_CONFIG_ID, + STANDARD_HARNESS_CONFIG_LABEL, STANDARD_HARNESS_CONFIG_MEMBERS, + STANDARD_HARNESS_PROMPT_TEMPLATE, SWARM_DELEGATE_AGENT_TYPES, SWARM_PLANNER_AGENT_TYPES, }; pub use openbitfun_agent_runtime::custom_agent::{ custom_agent_model_or_default, custom_agent_review_writable_tools, default_custom_agent_tools, @@ -82,7 +83,7 @@ static EMPTY_AGENT_TOOL_POLICY_OVERRIDES: std::sync::LazyLock = std::sync::LazyLock::new(PermissionConstraintLayer::default); -pub fn shared_coding_mode_tool_exposure_overrides() -> AgentToolPolicyOverrides { +pub fn standard_harness_tool_exposure_overrides() -> AgentToolPolicyOverrides { // Web research is a baseline capability of the shared coding modes; keep // WebSearch/WebFetch expanded so models do not need a GetToolSpec // unlock round-trip when switching between those modes. @@ -92,7 +93,7 @@ pub fn shared_coding_mode_tool_exposure_overrides() -> AgentToolPolicyOverrides overrides } -pub fn shared_coding_mode_tools() -> Vec { +pub fn standard_harness_tools() -> Vec { vec![ "Task".to_string(), "ListModels".to_string(), @@ -263,8 +264,9 @@ pub trait Agent: Send + Sync + 'static { #[cfg(test)] mod tests { use super::{ - get_embedded_prompt, shared_coding_mode_tool_exposure_overrides, shared_coding_mode_tools, - shared_coding_mode_user_context_policy, Agent, AgenticMode, MinimalMode, EMBEDDED_PROMPTS, + get_embedded_prompt, standard_harness_tool_exposure_overrides, standard_harness_tools, + standard_harness_user_context_policy, Agent, MinimalHarness, StandardHarness, + EMBEDDED_PROMPTS, }; #[test] @@ -278,14 +280,14 @@ mod tests { #[test] fn minimal_agent_prompt_resolves_to_embedded_prompt() { assert!( - get_embedded_prompt(MinimalMode::new().prompt_template_name(None)).is_some(), + get_embedded_prompt(MinimalHarness::new().prompt_template_name(None)).is_some(), "minimal Agent prompt must resolve through the embedded prompt catalog" ); } #[test] - fn shared_coding_mode_tools_exclude_create_plan_and_include_goal_tools() { - let tools = shared_coding_mode_tools(); + fn standard_harness_tools_exclude_create_plan_and_include_goal_tools() { + let tools = standard_harness_tools(); assert!(tools.contains(&"ListModels".to_string())); assert!(!tools.contains(&"CreatePlan".to_string())); @@ -294,15 +296,15 @@ mod tests { } #[test] - fn shared_coding_mode_tools_include_review_platform() { - let tools = shared_coding_mode_tools(); + fn standard_harness_tools_include_review_platform() { + let tools = standard_harness_tools(); assert!(tools.contains(&"ReviewPlatform".to_string())); } #[test] - fn shared_coding_mode_tools_keep_canvas_provider_tools_opt_in() { - let tools = shared_coding_mode_tools(); + fn standard_harness_tools_keep_canvas_provider_tools_opt_in() { + let tools = standard_harness_tools(); assert!(!tools.contains(&"CreateCanvas".to_string())); assert!(!tools.contains(&"ReadCanvas".to_string())); @@ -312,22 +314,22 @@ mod tests { #[test] fn agentic_mode_uses_shared_coding_tools() { - let shared_tools = shared_coding_mode_tools(); + let shared_tools = standard_harness_tools(); - assert_eq!(AgenticMode::new().default_tools(), shared_tools); + assert_eq!(StandardHarness::new().default_tools(), shared_tools); } #[test] fn agentic_mode_uses_shared_coding_user_context_policy() { - let shared_policy = shared_coding_mode_user_context_policy(); + let shared_policy = standard_harness_user_context_policy(); - assert_eq!(AgenticMode::new().user_context_policy(), shared_policy); + assert_eq!(StandardHarness::new().user_context_policy(), shared_policy); } #[test] fn agentic_mode_uses_shared_coding_tool_exposure_overrides() { - let shared_overrides = shared_coding_mode_tool_exposure_overrides(); - let agentic = AgenticMode::new(); + let shared_overrides = standard_harness_tool_exposure_overrides(); + let agentic = StandardHarness::new(); assert_eq!(agentic.tool_exposure_overrides(), &shared_overrides); } diff --git a/src/crates/assembly/core/src/agentic/agents/registry/availability.rs b/src/crates/assembly/core/src/agentic/agents/registry/availability.rs index 3627751a0e..c7d589b5bd 100644 --- a/src/crates/assembly/core/src/agentic/agents/registry/availability.rs +++ b/src/crates/assembly/core/src/agentic/agents/registry/availability.rs @@ -198,13 +198,17 @@ mod tests { .expect("builtin key"); let builtin_layers = resolve_override_layers( &builtin_entry, - Some("agentic"), + Some("Standard"), Some(&overrides( - "agentic", + "Standard", &builtin_key, AgentSubagentOverrideState::Disabled, )), - &overrides("agentic", &builtin_key, AgentSubagentOverrideState::Enabled), + &overrides( + "Standard", + &builtin_key, + AgentSubagentOverrideState::Enabled, + ), ); assert_eq!(builtin_layers.project_override, None); assert_eq!( @@ -217,13 +221,13 @@ mod tests { .expect("user key"); let user_layers = resolve_override_layers( &user_entry, - Some("agentic"), + Some("Standard"), Some(&overrides( - "agentic", + "Standard", &user_key, AgentSubagentOverrideState::Disabled, )), - &overrides("agentic", &user_key, AgentSubagentOverrideState::Enabled), + &overrides("Standard", &user_key, AgentSubagentOverrideState::Enabled), ); assert_eq!(user_layers.project_override, None); assert_eq!( @@ -239,13 +243,13 @@ mod tests { subagent_key_for(entry.subagent_source, entry.agent.as_ref()).expect("project key"); let layers = resolve_override_layers( &entry, - Some("agentic"), + Some("Standard"), Some(&overrides( - "agentic", + "Standard", &key, AgentSubagentOverrideState::Disabled, )), - &overrides("agentic", &key, AgentSubagentOverrideState::Enabled), + &overrides("Standard", &key, AgentSubagentOverrideState::Enabled), ); assert_eq!( diff --git a/src/crates/assembly/core/src/agentic/agents/registry/catalog.rs b/src/crates/assembly/core/src/agentic/agents/registry/catalog.rs index 586dc661ab..05b5938b9a 100644 --- a/src/crates/assembly/core/src/agentic/agents/registry/catalog.rs +++ b/src/crates/assembly/core/src/agentic/agents/registry/catalog.rs @@ -1,10 +1,10 @@ use super::types::AgentCategory; use super::visibility::SubagentVisibilityPolicy; use crate::agentic::agents::{ - Agent, AgenticMode, ClawMode, CodeReviewAgent, ComputerUseMode, CoworkMode, CreativeMode, + Agent, ClawMode, CodeReviewAgent, ComputerUseMode, CoworkMode, CreativeHarness, DeepResearchMode, DeepReviewAgent, ExploreAgent, GeneralPurposeAgent, GenerateDocAgent, - MinimalMode, ResearchSpecialistAgent, ReviewFixerAgent, ReviewJudgeAgent, ReviewWorkerAgent, - SwarmPlannerAgent, SwarmReviewerAgent, SwarmWorkerAgent, UltraMode, + MinimalHarness, ResearchSpecialistAgent, ReviewFixerAgent, ReviewJudgeAgent, ReviewWorkerAgent, + StandardHarness, SwarmPlannerAgent, SwarmReviewerAgent, SwarmWorkerAgent, UltimateHarness, }; use crate::agentic::memories::MemoryPhase2Agent; use openbitfun_agent_runtime::agents as runtime_agents; @@ -44,13 +44,13 @@ pub(crate) fn builtin_agent_specs_for_ids<'a>( fn builtin_agent_factory(id: &str) -> fn() -> Arc { match id { - "minimal" => || Arc::new(MinimalMode::new()), - "agentic" => || Arc::new(AgenticMode::new()), + "Minimal" => || Arc::new(MinimalHarness::new()), + "Standard" => || Arc::new(StandardHarness::new()), "Cowork" => || Arc::new(CoworkMode::new()), - "Creative" => || Arc::new(CreativeMode::new()), + "Creative" => || Arc::new(CreativeHarness::new()), "Claw" => || Arc::new(ClawMode::new()), "DeepResearch" => || Arc::new(DeepResearchMode::new()), - "Ultra" => || Arc::new(UltraMode::new()), + "Ultimate" => || Arc::new(UltimateHarness::new()), "SwarmPlanner" => || Arc::new(SwarmPlannerAgent::new()), "SwarmWorker" => || Arc::new(SwarmWorkerAgent::new()), "SwarmReviewer" => || Arc::new(SwarmReviewerAgent::new()), diff --git a/src/crates/assembly/core/src/agentic/agents/registry/mod.rs b/src/crates/assembly/core/src/agentic/agents/registry/mod.rs index 561a453dad..da315ec5f0 100644 --- a/src/crates/assembly/core/src/agentic/agents/registry/mod.rs +++ b/src/crates/assembly/core/src/agentic/agents/registry/mod.rs @@ -134,6 +134,14 @@ impl AgentRegistry { } } + let canonical = openbitfun_core_types::agent_identity::canonical_agent_id(agent_type); + if canonical != agent_type { + if let Some(entry) = self.read_agents().get(canonical).cloned() { + if entry.source == types::AgentSource::Builtin { + return Some(entry); + } + } + } let canonical = canonical_review_worker_agent_type(agent_type); (canonical != agent_type) .then(|| self.read_agents().get(canonical).cloned()) diff --git a/src/crates/assembly/core/src/agentic/agents/registry/resolution.rs b/src/crates/assembly/core/src/agentic/agents/registry/resolution.rs index 60f6661401..6dc2b64fe6 100644 --- a/src/crates/assembly/core/src/agentic/agents/registry/resolution.rs +++ b/src/crates/assembly/core/src/agentic/agents/registry/resolution.rs @@ -96,6 +96,6 @@ impl AgentRegistry { } pub fn default_agent_type(&self) -> &str { - "agentic" + "Standard" } } diff --git a/src/crates/assembly/core/src/agentic/agents/registry/tests.rs b/src/crates/assembly/core/src/agentic/agents/registry/tests.rs index c9ae3ce6e3..f07566b4ed 100644 --- a/src/crates/assembly/core/src/agentic/agents/registry/tests.rs +++ b/src/crates/assembly/core/src/agentic/agents/registry/tests.rs @@ -236,12 +236,12 @@ async fn review_lookup_cold_loads_the_requested_project_registry() { #[test] fn top_level_modes_default_to_auto() { for agent_type in [ - "agentic", + "Standard", "Cowork", "Creative", "Claw", "DeepResearch", - "Ultra", + "Ultimate", ] { assert_eq!(default_model_id_for_builtin_agent(agent_type), "primary"); } @@ -270,7 +270,7 @@ fn registry_exposes_sdk_agent_ids_without_leaking_core_agent_details() { let global_agent_ids = RuntimeAgentRegistry::agent_ids(®istry, RuntimeAgentRegistryQuery::default()); - assert!(global_agent_ids.contains(&"agentic".to_string())); + assert!(global_agent_ids.contains(&"Standard".to_string())); assert!(global_agent_ids.contains(&"Explore".to_string())); assert!(!global_agent_ids.contains(&"ProjectReviewer".to_string())); assert!(!global_agent_ids.contains(&"OtherProjectReviewer".to_string())); @@ -329,7 +329,7 @@ fn every_builtin_standard_mode_defaults_to_the_thread_goal_lifecycle() { .filter(|spec| spec.category == AgentCategory::Mode) { let mode = (spec.factory)(); - if matches!(mode.id(), "minimal" | "Ultra") { + if matches!(mode.id(), "Minimal" | "Ultimate") { continue; } let default_tools = mode.default_tools(); @@ -433,7 +433,7 @@ fn agent_list_and_delete_are_exposed_only_to_swarm_planners() { .default_tools() .iter() .any(|tool| tool == "AgentDelete"); - let should_have_controls = matches!(agent.id(), "Ultra" | "SwarmPlanner"); + let should_have_controls = matches!(agent.id(), "Ultimate" | "SwarmPlanner"); assert_eq!( has_list, should_have_controls, @@ -455,13 +455,13 @@ fn skill_tool_is_exposed_only_to_ultra_and_swarm_worker_within_the_swarm_catalog let agent = (spec.factory)(); if !matches!( agent.id(), - "Ultra" | "SwarmPlanner" | "SwarmWorker" | "SwarmReviewer" + "Ultimate" | "SwarmPlanner" | "SwarmWorker" | "SwarmReviewer" ) { continue; } assert_eq!( agent.default_tools().iter().any(|tool| tool == "Skill"), - matches!(agent.id(), "Ultra" | "SwarmWorker"), + matches!(agent.id(), "Ultimate" | "SwarmWorker"), "unexpected Skill exposure for {}", agent.id() ); @@ -620,7 +620,7 @@ async fn task_visible_subagents_are_filtered_by_parent_agent() { let agentic_visible = registry .get_subagents_for_query(&SubagentQueryContext { - parent_agent_type: Some("agentic"), + parent_agent_type: Some("Standard"), workspace_root: None, list_scope: SubagentListScope::TaskVisible, include_disabled: false, @@ -681,7 +681,7 @@ async fn task_visible_subagents_are_filtered_by_parent_agent() { let ultra_visible = registry .get_subagents_for_query(&SubagentQueryContext { - parent_agent_type: Some("Ultra"), + parent_agent_type: Some("Ultimate"), workspace_root: None, list_scope: SubagentListScope::TaskVisible, include_disabled: false, @@ -906,7 +906,7 @@ async fn parent_subagent_overrides_follow_source_scopes() { .insert(workspace.clone(), project_entries); let builtin_query = SubagentQueryContext { - parent_agent_type: Some("agentic"), + parent_agent_type: Some("Standard"), workspace_root: Some(&workspace), list_scope: SubagentListScope::RegistryManagement, include_disabled: true, @@ -932,7 +932,7 @@ async fn parent_subagent_overrides_follow_source_scopes() { ); let mut project_overrides = HashMap::new(); project_overrides.insert( - resolve_mode_config_profile_id("agentic").into_owned(), + resolve_mode_config_profile_id("Standard").into_owned(), project_parent_map, ); @@ -945,7 +945,7 @@ async fn parent_subagent_overrides_follow_source_scopes() { user_parent_map.insert(builtin_override_key, AgentSubagentOverrideState::Disabled); let mut user_overrides = HashMap::new(); user_overrides.insert( - resolve_mode_config_profile_id("agentic").into_owned(), + resolve_mode_config_profile_id("Standard").into_owned(), user_parent_map, ); @@ -1402,7 +1402,7 @@ async fn external_routes_are_workspace_scoped_fail_closed_and_generation_leased( let local_only = registry .get_subagents_for_query(&SubagentQueryContext { - parent_agent_type: Some("agentic"), + parent_agent_type: Some("Standard"), workspace_root: Some(&workspace), list_scope: SubagentListScope::TaskVisible, include_disabled: false, @@ -1415,7 +1415,7 @@ async fn external_routes_are_workspace_scoped_fail_closed_and_generation_leased( let external = registry .get_subagents_for_query(&SubagentQueryContext { - parent_agent_type: Some("agentic"), + parent_agent_type: Some("Standard"), workspace_root: Some(&workspace), list_scope: SubagentListScope::TaskVisible, include_disabled: false, @@ -1620,7 +1620,7 @@ fn persisted_external_owner_never_falls_back_to_a_same_name_local_mode() { assert!(registry .resolve_primary_agent_for_turn( - "agentic", + "Standard", Some(Path::new("C:/workspace/restarted-external-owner")), true, Some(openbitfun_core_types::SessionAgentRouteOwner::External), @@ -1628,7 +1628,7 @@ fn persisted_external_owner_never_falls_back_to_a_same_name_local_mode() { .is_none()); let local = registry .resolve_primary_agent_for_turn( - "agentic", + "Standard", Some(Path::new("C:/workspace/legacy-local-owner")), true, Some(openbitfun_core_types::SessionAgentRouteOwner::Local), @@ -1683,7 +1683,7 @@ async fn external_agent_role_controls_main_and_task_projection() { .expect("external primary projection should be visible"); assert!(!registry .get_subagents_for_query(&SubagentQueryContext { - parent_agent_type: Some("agentic"), + parent_agent_type: Some("Standard"), workspace_root: Some(&workspace), list_scope: SubagentListScope::TaskVisible, include_disabled: false, @@ -1708,7 +1708,7 @@ async fn external_agent_role_controls_main_and_task_projection() { .any(|agent| agent.id == logical_id)); let subagent = registry .get_subagents_for_query(&SubagentQueryContext { - parent_agent_type: Some("agentic"), + parent_agent_type: Some("Standard"), workspace_root: Some(&workspace), list_scope: SubagentListScope::TaskVisible, include_disabled: false, @@ -1727,7 +1727,7 @@ async fn external_agent_role_controls_main_and_task_projection() { fn persisted_primary_route_owner_rejects_same_name_route_takeover() { let registry = AgentRegistry::new(); let workspace = PathBuf::from("D:/workspace/owner-takeover"); - let logical_id = "agentic"; + let logical_id = "Standard"; let runtime_key = "external::agentic"; registry.install_external_subagent_routes( &workspace, @@ -1782,7 +1782,7 @@ fn persisted_primary_route_owner_rejects_same_name_route_takeover() { fn validated_generation_replacement_restores_same_name_local_agent() { let registry = AgentRegistry::new(); let workspace = PathBuf::from("D:/workspace/plugin-agent-removed"); - let logical_id = "agentic"; + let logical_id = "Standard"; let runtime_key = "external::agentic::generation-1"; registry.install_external_subagent_routes( &workspace, @@ -1857,11 +1857,11 @@ fn route_overlay_overrides_without_replacing_base_external_routes() { registry.install_external_subagent_routes( &workspace, vec![ - registration("external::base-agentic", "agentic", "Base", "claude-code"), + registration("external::base-agentic", "Standard", "Base", "claude-code"), registration("external::base-only", "base-only", "Base", "claude-code"), ], [ - route("agentic", "external::base-agentic"), + route("Standard", "external::base-agentic"), route("base-only", "external::base-only"), ] .into_iter() @@ -1871,11 +1871,11 @@ fn route_overlay_overrides_without_replacing_base_external_routes() { &workspace, "opencode-plugin-config", vec![ - registration("external::plugin-agentic", "agentic", "Plugin", "opencode"), + registration("external::plugin-agentic", "Standard", "Plugin", "opencode"), registration("external::plugin-only", "plugin-only", "Plugin", "opencode"), ], [ - route("agentic", "external::plugin-agentic"), + route("Standard", "external::plugin-agentic"), route("plugin-only", "external::plugin-only"), ] .into_iter() @@ -1883,7 +1883,7 @@ fn route_overlay_overrides_without_replacing_base_external_routes() { ); let plugin_turn = registry - .resolve_primary_agent_for_turn("agentic", Some(&workspace), true, None) + .resolve_primary_agent_for_turn("Standard", Some(&workspace), true, None) .expect("overlay route"); assert_eq!(plugin_turn.runtime_agent_key, "external::plugin-agentic"); assert_eq!( @@ -1906,14 +1906,14 @@ fn route_overlay_overrides_without_replacing_base_external_routes() { vec![ registration( "external::base-agentic-v2", - "agentic", + "Standard", "Base v2", "claude-code", ), registration("external::base-only", "base-only", "Base", "claude-code"), ], [ - route("agentic", "external::base-agentic-v2"), + route("Standard", "external::base-agentic-v2"), route("base-only", "external::base-only"), ] .into_iter() @@ -1921,7 +1921,7 @@ fn route_overlay_overrides_without_replacing_base_external_routes() { ); assert_eq!( registry - .resolve_primary_agent_for_turn("agentic", Some(&workspace), true, None) + .resolve_primary_agent_for_turn("Standard", Some(&workspace), true, None) .expect("overlay still wins after base refresh") .runtime_agent_key, "external::plugin-agentic" @@ -1931,7 +1931,7 @@ fn route_overlay_overrides_without_replacing_base_external_routes() { assert_eq!( registry - .resolve_primary_agent_for_turn("agentic", Some(&workspace), true, None) + .resolve_primary_agent_for_turn("Standard", Some(&workspace), true, None) .expect("latest base route restored") .runtime_agent_key, "external::base-agentic-v2" @@ -1955,7 +1955,7 @@ fn route_overlay_overrides_without_replacing_base_external_routes() { fn persisted_route_key_rejects_same_name_external_provider_takeover() { let registry = AgentRegistry::new(); let workspace = PathBuf::from("D:/workspace/plugin-agent-takeover"); - let logical_id = "agentic"; + let logical_id = "Standard"; let registration = |runtime_key: &str, route_key: &str| ExternalSubagentRegistration { runtime_key: runtime_key.to_string(), logical_id: logical_id.to_string(), diff --git a/src/crates/assembly/core/src/agentic/coordination/background_outcomes.rs b/src/crates/assembly/core/src/agentic/coordination/background_outcomes.rs index af1f919bc5..792d839446 100644 --- a/src/crates/assembly/core/src/agentic/coordination/background_outcomes.rs +++ b/src/crates/assembly/core/src/agentic/coordination/background_outcomes.rs @@ -695,7 +695,7 @@ mod tests { .create_session_with_id( Some("parent-session".to_string()), "Parent".to_string(), - "agentic".to_string(), + "Standard".to_string(), config.clone(), ) .await diff --git a/src/crates/assembly/core/src/agentic/coordination/coordination_store.rs b/src/crates/assembly/core/src/agentic/coordination/coordination_store.rs index 5a58a1697e..4cdc5dd8e4 100644 --- a/src/crates/assembly/core/src/agentic/coordination/coordination_store.rs +++ b/src/crates/assembly/core/src/agentic/coordination/coordination_store.rs @@ -2,6 +2,7 @@ use crate::service::coordination_persistence::{ initialize_coordination_schema, validate_coordination_agent_id, }; use crate::util::errors::{OpenBitFunError, OpenBitFunResult}; +use openbitfun_core_types::agent_identity::{canonical_agent_id, HarnessId}; use rusqlite::{params, Connection, OptionalExtension, Transaction, TransactionBehavior}; use std::path::PathBuf; use std::sync::{Arc, Mutex}; @@ -331,7 +332,7 @@ ORDER BY swarm_nodes.created_at_ms ASC, agents.agent_pk ASC .map_err(db_error)?; let root_session_id = match root_session_id { Some(root_session_id) => root_session_id, - None if parent_agent_type == "Ultra" => parent_session_id.clone(), + None if parent_agent_type == "Ultimate" => parent_session_id.clone(), None => { return Err(OpenBitFunError::tool( "Swarm parent is not part of the current tree".to_string(), @@ -346,8 +347,8 @@ ORDER BY swarm_nodes.created_at_ms ASC, agents.agent_pk ASC .map_err(db_error)?; transaction .execute( - "INSERT OR IGNORE INTO swarm_nodes (session_id, root_session_id, parent_session_id, agent_type, depth, created_at_ms) VALUES (?1, ?1, NULL, 'Ultra', 0, ?2)", - params![root_session_id, unix_time_ms() as i64], + "INSERT OR IGNORE INTO swarm_nodes (session_id, root_session_id, parent_session_id, agent_type, depth, created_at_ms) VALUES (?1, ?1, NULL, ?2, 0, ?3)", + params![root_session_id, HarnessId::Ultimate.as_str(), unix_time_ms() as i64], ) .map_err(db_error)?; @@ -355,7 +356,14 @@ ORDER BY swarm_nodes.created_at_ms ASC, agents.agent_pk ASC .query_row( "SELECT root_session_id, depth, agent_type FROM swarm_nodes WHERE session_id = ?1", params![parent_session_id], - |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?, row.get::<_, String>(2)?)), + |row| { + let agent_type = row.get::<_, String>(2)?; + Ok(( + row.get::<_, String>(0)?, + row.get::<_, i64>(1)?, + canonical_agent_id(&agent_type).to_owned(), + )) + }, ) .optional() .map_err(db_error)? @@ -370,7 +378,7 @@ ORDER BY swarm_nodes.created_at_ms ASC, agents.agent_pk ASC "Swarm child depth does not match its parent lineage".to_string(), )); } - if !matches!(parent.2.as_str(), "Ultra" | "SwarmPlanner") { + if !matches!(parent.2.as_str(), "Ultimate" | "SwarmPlanner") { return Err(OpenBitFunError::tool( "Only a Swarm planner can launch child agents".to_string(), )); @@ -1427,11 +1435,69 @@ PRAGMA user_version = 2; ); } + #[tokio::test] + async fn swarm_root_identity_preserves_legacy_trees_and_writes_canonical_new_roots() { + let (_root, store) = test_store(); + store + .with_connection(|connection| { + connection + .execute_batch( + "INSERT INTO swarm_trees VALUES ('legacy-root', 1); + INSERT INTO swarm_nodes VALUES ('legacy-root', 'legacy-root', NULL, 'Ultra', 0, 1); + INSERT INTO swarm_nodes VALUES ('existing-planner', 'legacy-root', 'legacy-root', 'SwarmPlanner', 1, 1);", + ) + .map_err(db_error) + }) + .await + .expect("seed a legacy tree"); + + store + .reserve_swarm_child("legacy-root", "new-planner", "Ultimate", "SwarmPlanner", 1) + .await + .expect("a legacy root remains usable after an upgrade"); + store + .reserve_swarm_child( + "current-root", + "current-planner", + "Ultimate", + "SwarmPlanner", + 1, + ) + .await + .expect("create a tree with the canonical identity"); + + assert_eq!( + store + .swarm_depth_for_session("existing-planner") + .await + .unwrap(), + Some(1) + ); + store + .with_connection(|connection| { + for (session_id, expected) in + [("legacy-root", "Ultra"), ("current-root", "Ultimate")] + { + let stored_identity: String = connection + .query_row( + "SELECT agent_type FROM swarm_nodes WHERE session_id = ?1", + params![session_id], + |row| row.get(0), + ) + .map_err(db_error)?; + assert_eq!(stored_identity, expected); + } + Ok(()) + }) + .await + .expect("legacy rows remain intact and new rows use canonical identities"); + } + #[tokio::test] async fn swarm_admission_enforces_depth_and_tree_size_budgets() { let (_root, store) = test_store(); store - .reserve_swarm_child("root", "planner", "Ultra", "SwarmPlanner", 1) + .reserve_swarm_child("root", "planner", "Ultimate", "SwarmPlanner", 1) .await .expect("reserve planner"); store @@ -1445,7 +1511,7 @@ PRAGMA user_version = 2; .await .expect_err("a non-Ultra session cannot create a new Swarm tree"); store - .reserve_swarm_child("planner", "spoofed-worker", "Ultra", "SwarmWorker", 2) + .reserve_swarm_child("planner", "spoofed-worker", "Ultimate", "SwarmWorker", 2) .await .expect_err("the runtime parent type must match the persisted tree node"); store @@ -1603,7 +1669,7 @@ PRAGMA user_version = 2; async fn direct_child_agents_use_latest_status_and_ignore_delivery() { let (_root, store) = test_store(); store - .reserve_swarm_child("root", "planner", "Ultra", "SwarmPlanner", 1) + .reserve_swarm_child("root", "planner", "Ultimate", "SwarmPlanner", 1) .await .expect("reserve planner"); store @@ -1675,7 +1741,7 @@ PRAGMA user_version = 2; async fn direct_child_resolution_and_subtree_postorder_are_lineage_scoped() { let (_root, store) = test_store(); store - .reserve_swarm_child("root", "planner", "Ultra", "SwarmPlanner", 1) + .reserve_swarm_child("root", "planner", "Ultimate", "SwarmPlanner", 1) .await .expect("reserve planner"); store diff --git a/src/crates/assembly/core/src/agentic/coordination/coordinator.rs b/src/crates/assembly/core/src/agentic/coordination/coordinator.rs index 960b41820d..c491ca2151 100644 --- a/src/crates/assembly/core/src/agentic/coordination/coordinator.rs +++ b/src/crates/assembly/core/src/agentic/coordination/coordinator.rs @@ -1027,7 +1027,7 @@ fn delegation_policy_for_agent_turn( swarm_depth: Option, ) -> OpenBitFunResult { match agent_type { - "Ultra" => Ok(DelegationPolicy::swarm_root()), + "Ultimate" => Ok(DelegationPolicy::swarm_root()), "SwarmPlanner" => { let nesting_depth = swarm_depth.ok_or_else(|| { OpenBitFunError::tool( @@ -1615,9 +1615,9 @@ impl ConversationCoordinator { fn normalize_agent_type(agent_type: &str) -> String { if agent_type.trim().is_empty() { - "agentic".to_string() + "Standard".to_string() } else { - agent_type.trim().to_string() + openbitfun_core_types::agent_identity::canonical_agent_id(agent_type.trim()).to_string() } } @@ -2957,7 +2957,7 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet let metadata = SessionMetadata { session_id: session_id.to_string(), session_name: "Recovered Session".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), last_user_dialog_agent_type: None, last_submitted_agent_type: None, created_by: None, @@ -5084,12 +5084,12 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet Some(session) => { let agent_type = session.agent_type.trim(); if agent_type.is_empty() { - "agentic".to_string() + "Standard".to_string() } else { agent_type.to_string() } } - None => "agentic".to_string(), + None => "Standard".to_string(), }; let workspace_path = self .require_main_session_workspace(session_id) @@ -5260,12 +5260,12 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet Some(session) => { let agent_type = session.agent_type.trim(); if agent_type.is_empty() { - "agentic".to_string() + "Standard".to_string() } else { agent_type.to_string() } } - None => "agentic".to_string(), + None => "Standard".to_string(), }; let workspace_path = self .require_main_session_workspace(session_id) @@ -5939,7 +5939,7 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet } else if !session.agent_type.is_empty() { session.agent_type.clone() } else { - "agentic".to_string() + "Standard".to_string() }; let effective_agent_type = Self::normalize_agent_type(&provisional_agent_type); let primary_agent_binding = Self::resolve_session_primary_agent( @@ -15240,8 +15240,8 @@ mod tests { #[test] fn agent_turn_delegation_policy_recovers_swarm_scope_and_depth() { - let ultra = delegation_policy_for_agent_turn("Ultra", None) - .expect("Ultra should start a Swarm tree"); + let ultra = delegation_policy_for_agent_turn("Ultimate", None) + .expect("Ultimate should start a Swarm tree"); assert!(ultra.allow_subagent_spawn); assert_eq!(ultra.nesting_depth, 0); assert_eq!( @@ -15365,7 +15365,7 @@ mod tests { .create_session_with_id( Some(session_id.clone()), "Cancellation fault".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), ..Default::default() @@ -15409,7 +15409,7 @@ mod tests { round_id: "round-1".to_string(), attempt_id: None, attempt_index: None, - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace: None, primary_model_facts: Default::default(), context_vars: HashMap::new(), @@ -15474,7 +15474,7 @@ mod tests { let summary = runtime_session_summary(openbitfun_agent_runtime::session::SessionSummary { session_id: "session".to_string(), session_name: "Session".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), model_id: Some("fast".to_string()), reasoning_preset: Some("high".to_string()), last_user_dialog_agent_type: None, @@ -15622,20 +15622,19 @@ mod tests { .expect("session remains loaded"); let workspace = ConversationCoordinator::build_workspace_binding(&session.config).await; - let binding = - ConversationCoordinator::resolve_session_primary_agent(&session, "agentic", &workspace) - .await - .expect( - "explicitly selected local mode should resolve independently of the old owner", - ); + let binding = ConversationCoordinator::resolve_session_primary_agent( + &session, "Standard", &workspace, + ) + .await + .expect("explicitly selected local mode should resolve independently of the old owner"); - assert_eq!(binding.runtime_agent_key, "agentic"); + assert_eq!(binding.runtime_agent_key, "Standard"); assert_eq!(binding.route_owner, SessionAgentRouteOwner::Local); session_manager .update_session_agent_binding( &session_id, - "AGENTIC", + "STANDARD", SessionAgentRouteOwner::External, Some("test:external".to_string()), ) @@ -15646,7 +15645,7 @@ mod tests { .expect("case-variant session remains loaded"); let error = match ConversationCoordinator::resolve_session_primary_agent( &case_variant_session, - "agentic", + "Standard", &workspace, ) .await @@ -15772,7 +15771,7 @@ mod tests { let session = session_manager .create_session( "Cancelled compaction".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), ..SessionConfig::default() @@ -15805,7 +15804,7 @@ mod tests { session_id.clone(), turn_id.clone(), 0, - "agentic".to_string(), + "Standard".to_string(), None, None, None, @@ -15851,7 +15850,7 @@ mod tests { let session = session_manager .create_session( "Persistence failure".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().into_owned()), ..Default::default() @@ -16283,7 +16282,7 @@ mod tests { &coordinator, AgentSessionModeUpdateRequest { session_id: "missing-session".to_string(), - mode_id: "agentic".to_string(), + mode_id: "Standard".to_string(), agent_route_key: None, }, ) @@ -16314,7 +16313,7 @@ mod tests { coordinator.create_session_with_workspace( None, "Runtime mode validation".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path_string.clone()), ..Default::default() @@ -16360,7 +16359,7 @@ mod tests { coordinator.create_session_with_workspace( None, "Runtime mode validation".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path_string.clone()), ..Default::default() @@ -16407,7 +16406,7 @@ mod tests { coordinator.create_session_with_workspace( None, "Runtime mode update".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path_string.clone()), ..Default::default() @@ -16460,7 +16459,7 @@ mod tests { coordinator.create_session_with_workspace( None, "Runtime model update".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path_string.clone()), model_id: Some("primary".to_string()), @@ -16744,7 +16743,7 @@ mod tests { let session = session_manager .create_session( "Durable completion".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), ..Default::default() @@ -16755,7 +16754,7 @@ mod tests { let turn_id = session_manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "finish".to_string(), Some("turn-durable-fence".to_string()), None, @@ -16818,7 +16817,7 @@ mod tests { .create_transient_session_with_id_and_details( Some("transient-session".to_string()), "Transient completion".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), ..Default::default() @@ -16831,7 +16830,7 @@ mod tests { let turn_id = session_manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "finish".to_string(), Some("turn-transient-result".to_string()), None, @@ -16881,7 +16880,7 @@ mod tests { let cancelled_turn_id = session_manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "cancel".to_string(), Some("turn-transient-cancelled".to_string()), None, @@ -16908,7 +16907,7 @@ mod tests { let failed_turn_id = session_manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "fail".to_string(), Some("turn-transient-failed".to_string()), None, @@ -16943,7 +16942,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Reverted".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().into_owned()), ..Default::default() @@ -16955,7 +16954,7 @@ mod tests { session_manager .start_dialog_turn( session_id, - "agentic".to_string(), + "Standard".to_string(), prompt.to_string(), Some(turn_id.to_string()), None, @@ -17406,7 +17405,7 @@ mod tests { let session = session_manager .create_session( "Transient transcript".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), ..Default::default() @@ -17472,7 +17471,7 @@ mod tests { .create_session_with_id( Some("ownership-conflict".to_string()), "blocked".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -17874,7 +17873,7 @@ mod tests { .create_hidden_subagent_session_with_workspace( Some("hidden-ownership-conflict".to_string()), "hidden".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig::default(), workspace_path.clone(), None, @@ -17971,7 +17970,7 @@ mod tests { let session = session_manager .create_session( "Shell turn".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), ..Default::default() @@ -18067,7 +18066,7 @@ mod tests { let session = session_manager .create_session( "Denied shell turn".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), ..Default::default() @@ -18125,7 +18124,7 @@ mod tests { let session = session_manager .create_session( "Failed shell turn".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), ..Default::default() @@ -18180,7 +18179,7 @@ mod tests { let session = session_manager .create_session( "Cancelled shell turn".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), ..Default::default() @@ -18956,7 +18955,7 @@ mod tests { &coordinator, AgentSessionCreateRequest { session_name: "Worker".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), agent_route_key: None, workspace_path: Some(workspace_path.to_string_lossy().into_owned()), project_workspace_path: None, @@ -18996,7 +18995,7 @@ mod tests { &coordinator, AgentSessionCreateRequest { session_name: "Original".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), agent_route_key: None, workspace_path: Some(workspace.clone()), project_workspace_path: None, @@ -19103,7 +19102,7 @@ mod tests { &coordinator, AgentSessionCreateRequest { session_name: "Over capacity".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), agent_route_key: None, workspace_path: Some(std::env::temp_dir().to_string_lossy().into_owned()), project_workspace_path: None, @@ -19135,7 +19134,7 @@ mod tests { "fixed-session-id".to_string(), AgentSessionCreateRequest { session_name: "Fixed worker".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), agent_route_key: None, workspace_path: Some(workspace_path.to_string_lossy().into_owned()), project_workspace_path: None, @@ -19170,7 +19169,7 @@ mod tests { "fixed-session-id".to_string(), AgentSessionCreateRequest { session_name: "Duplicate worker".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), agent_route_key: None, workspace_path: Some(workspace_path.to_string_lossy().into_owned()), project_workspace_path: None, @@ -19281,7 +19280,7 @@ mod tests { let mut metadata = SessionMetadata::new( session_id.clone(), format!("Remote {index}"), - "agentic".to_string(), + "Standard".to_string(), "primary".to_string(), ); metadata.custom_metadata = Some(thread_goal_patch(&goal)); @@ -19325,7 +19324,7 @@ mod tests { .create_session_with_id( Some(loaded_session_id.clone()), "Loaded remote A".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(logical_workspace_path.to_string()), remote_connection_id: Some(remote_identities[0].0.clone()), @@ -19356,7 +19355,7 @@ mod tests { let mut loaded_metadata = SessionMetadata::new( loaded_session_id.clone(), "Loaded remote A".to_string(), - "agentic".to_string(), + "Standard".to_string(), "primary".to_string(), ); loaded_metadata.custom_metadata = Some(thread_goal_patch(&loaded_goal_fixture)); @@ -19440,7 +19439,7 @@ mod tests { .create_session_with_id( Some(session_id.clone()), "Remote goal mutation".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(logical_workspace_path.clone()), remote_connection_id: Some(remote_connection_id), @@ -19503,7 +19502,7 @@ mod tests { coordinator.create_session_with_workspace( None, "First".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path_string.clone()), ..Default::default() @@ -19523,7 +19522,7 @@ mod tests { coordinator.create_session_with_workspace( None, "Second".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path_string.clone()), ..Default::default() @@ -19558,7 +19557,7 @@ mod tests { coordinator.create_session_with_workspace( None, "Explicit".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path_string.clone()), model_id: Some("explicit-model".to_string()), @@ -19585,7 +19584,7 @@ mod tests { let workspace = workspace_path.to_string_lossy().into_owned(); let request = |name: &str| AgentSessionCreateRequest { session_name: name.to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), agent_route_key: None, workspace_path: Some(workspace.clone()), project_workspace_path: None, @@ -19719,7 +19718,7 @@ mod tests { "../other-session".to_string(), AgentSessionCreateRequest { session_name: "Invalid worker".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), agent_route_key: None, workspace_path: Some(std::env::temp_dir().to_string_lossy().into_owned()), project_workspace_path: None, @@ -19793,7 +19792,7 @@ mod tests { let parent_session = session_manager .create_session( "Parent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { model_id: Some("primary".to_string()), workspace_path: Some(workspace_path.to_string_lossy().into_owned()), @@ -19905,7 +19904,7 @@ mod tests { let parent_session = session_manager .create_session( "Parent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { model_id: Some("primary".to_string()), workspace_path: Some(workspace_path.clone()), @@ -19984,7 +19983,7 @@ mod tests { .create_transient_session_with_id_and_details( None, "Transient parent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { model_id: Some("primary".to_string()), workspace_path: Some(workspace.clone()), @@ -20125,7 +20124,7 @@ mod tests { let parent_session = session_manager .create_session( "Parent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { model_id: Some("primary".to_string()), workspace_path: Some(workspace_path.to_string_lossy().into_owned()), @@ -20139,7 +20138,7 @@ mod tests { .create_hidden_agent_session( None, "Reusable subagent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { model_id: Some("parent-model".to_string()), workspace_path: Some(workspace_path.to_string_lossy().into_owned()), @@ -20289,7 +20288,7 @@ mod tests { let parent_session = session_manager .create_session( "Parent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { model_id: Some("primary".to_string()), workspace_path: Some(workspace_path.to_string_lossy().into_owned()), @@ -20441,7 +20440,7 @@ mod tests { .create_hidden_agent_session( None, "Reusable subagent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path.to_string_lossy().into_owned()), ..Default::default() @@ -20461,7 +20460,7 @@ mod tests { let turn_id = session_manager .start_dialog_turn_with_existing_context( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "continue investigation".to_string(), Some("subagent-turn-reuse".to_string()), None, @@ -20519,7 +20518,7 @@ mod tests { let parent_session = session_manager .create_session( "Parent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path.to_string_lossy().into_owned()), remote_connection_id: Some("ssh-user@example.test:22".to_string()), @@ -20532,8 +20531,8 @@ mod tests { session_manager .inherit_session_agent_type_state( &parent_session.session_id, - Some("agentic".to_string()), - Some("agentic".to_string()), + Some("Standard".to_string()), + Some("Standard".to_string()), ) .await .expect("parent agent type state should be set"); @@ -20597,11 +20596,11 @@ mod tests { ); assert_eq!( child_session.last_user_dialog_agent_type.as_deref(), - Some("agentic") + Some("Standard") ); assert_eq!( child_session.last_submitted_agent_type.as_deref(), - Some("agentic") + Some("Standard") ); assert_eq!( child_session.config.remote_connection_id.as_deref(), @@ -20778,7 +20777,7 @@ mod tests { "deepReviewRunManifest": { "reviewTargetEvidence": { "version": 1 } } }); - assert!(turn_review_manifest_for_agent(Some(&metadata), "agentic").is_none()); + assert!(turn_review_manifest_for_agent(Some(&metadata), "Standard").is_none()); assert!(turn_review_manifest_for_agent(Some(&metadata), "CodeReview").is_some()); assert!(turn_review_manifest_for_agent(Some(&metadata), "DeepReview").is_some()); } diff --git a/src/crates/assembly/core/src/agentic/coordination/scheduler.rs b/src/crates/assembly/core/src/agentic/coordination/scheduler.rs index 061a86a60a..d9dab519d2 100644 --- a/src/crates/assembly/core/src/agentic/coordination/scheduler.rs +++ b/src/crates/assembly/core/src/agentic/coordination/scheduler.rs @@ -1083,7 +1083,7 @@ impl DialogScheduler { }; let agent_type = session.agent_type.trim(); if agent_type.is_empty() { - Ok("agentic".to_string()) + Ok("Standard".to_string()) } else { Ok(agent_type.to_string()) } @@ -3279,7 +3279,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Reverted".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().into_owned()), ..Default::default() @@ -3341,7 +3341,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Parent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().into_owned()), ..Default::default() @@ -3363,7 +3363,7 @@ mod tests { scheduler .deliver_background_result( session_id.to_string(), - "agentic".to_string(), + "Standard".to_string(), None, None, None, @@ -3401,7 +3401,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "External parent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().into_owned()), model_id: Some("model-original".to_string()), @@ -3456,7 +3456,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Interrupted".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().into_owned()), ..Default::default() @@ -3467,7 +3467,7 @@ mod tests { session_manager .start_dialog_turn( session_id, - "agentic".to_string(), + "Standard".to_string(), "finish this".to_string(), Some(turn_id.to_string()), None, @@ -3533,7 +3533,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Goal parent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().into_owned()), ..Default::default() @@ -3555,7 +3555,7 @@ mod tests { scheduler .deliver_thread_goal_objective_updated( session_id.to_string(), - "agentic".to_string(), + "Standard".to_string(), None, None, None, @@ -3591,7 +3591,7 @@ mod tests { original_user_input: None, prepended_messages: Vec::new(), turn_id: Some(turn_id.to_string()), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some("/workspace".to_string()), remote_connection_id: None, remote_ssh_host: None, @@ -3664,7 +3664,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Interrupted queue hold".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().into_owned()), ..Default::default() @@ -3675,7 +3675,7 @@ mod tests { session_manager .start_dialog_turn( session_id, - "agentic".to_string(), + "Standard".to_string(), "original work".to_string(), Some(turn_id.to_string()), None, @@ -3720,7 +3720,7 @@ mod tests { .create_session_with_id( Some(parent_session_id.to_string()), "Parent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().to_string()), ..Default::default() @@ -3798,7 +3798,7 @@ mod tests { original_message: None, turn_id: Some("missing-turn".to_string()), execution: Default::default(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some(workspace.to_string_lossy().to_string()), remote_connection_id: None, remote_ssh_host: None, @@ -3837,7 +3837,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Queued".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().to_string()), ..Default::default() @@ -3864,7 +3864,7 @@ mod tests { original_message: None, turn_id: Some(turn_id.to_string()), execution: Default::default(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: None, remote_connection_id: None, remote_ssh_host: None, @@ -3913,7 +3913,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Delegated".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().to_string()), ..Default::default() @@ -3944,7 +3944,7 @@ mod tests { ecosystem_id: "opencode".to_string(), logical_id: "reviewer".to_string(), }, - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: None, remote_connection_id: None, remote_ssh_host: None, @@ -3972,7 +3972,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Delegated".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().to_string()), ..Default::default() @@ -3999,7 +3999,7 @@ mod tests { original_message: None, turn_id: Some("queued-turn".to_string()), execution: Default::default(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: None, remote_connection_id: None, remote_ssh_host: None, @@ -4034,7 +4034,7 @@ mod tests { ecosystem_id: "opencode".to_string(), logical_id: "reviewer".to_string(), }, - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: None, remote_connection_id: None, remote_ssh_host: None, @@ -4066,7 +4066,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "ACP".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().to_string()), ..Default::default() @@ -4093,7 +4093,7 @@ mod tests { original_message: None, turn_id: Some("rejected-turn".to_string()), execution: Default::default(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: None, remote_connection_id: None, remote_ssh_host: None, @@ -4136,7 +4136,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Duplicate".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().to_string()), ..Default::default() @@ -4166,7 +4166,7 @@ mod tests { original_message: None, turn_id: Some(turn_id.to_string()), execution: Default::default(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: None, remote_connection_id: None, remote_ssh_host: None, @@ -4195,7 +4195,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Workspace".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_a.to_string_lossy().to_string()), ..Default::default() @@ -4211,7 +4211,7 @@ mod tests { original_message: None, turn_id: Some(turn_id.to_string()), execution: Default::default(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some(workspace_b.to_string_lossy().to_string()), remote_connection_id: None, remote_ssh_host: None, @@ -4245,7 +4245,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Invalid agent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().to_string()), ..Default::default() @@ -4289,7 +4289,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Known turn".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().to_string()), ..Default::default() @@ -4300,7 +4300,7 @@ mod tests { session_manager .start_dialog_turn( session_id, - "agentic".to_string(), + "Standard".to_string(), "hello".to_string(), Some(turn_id.to_string()), None, @@ -4335,7 +4335,7 @@ mod tests { Some("/workspace".to_string()), None, None, - "agentic".to_string(), + "Standard".to_string(), "hello".to_string(), None, DialogSubmissionPolicy::for_source(DialogTriggerSource::DesktopUi), @@ -4355,7 +4355,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Steering".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().into_owned()), ..Default::default() @@ -4559,7 +4559,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Maintenance retire".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.to_string_lossy().to_string()), ..Default::default() @@ -4628,7 +4628,7 @@ mod tests { Some("/workspace".to_string()), None, None, - "agentic".to_string(), + "Standard".to_string(), "hello".to_string(), None, DialogSubmissionPolicy::for_source(DialogTriggerSource::AgentSession), diff --git a/src/crates/assembly/core/src/agentic/execution/edit_constraint_guard/tests.rs b/src/crates/assembly/core/src/agentic/execution/edit_constraint_guard/tests.rs index a01307eb96..ef6e511bff 100644 --- a/src/crates/assembly/core/src/agentic/execution/edit_constraint_guard/tests.rs +++ b/src/crates/assembly/core/src/agentic/execution/edit_constraint_guard/tests.rs @@ -847,7 +847,7 @@ fn local_recursive_delete_fallback_finds_protected_descendant() { fs::write(target.join("tests/example.rs"), "test").expect("create test file"); let context = ToolUseContext { tool_call_id: Some("tool-call-1".to_string()), - agent_type: Some("agentic".to_string()), + agent_type: Some("Standard".to_string()), session_id: None, dialog_turn_id: Some("turn-1".to_string()), workspace: Some(WorkspaceBinding::new(None, root.clone())), diff --git a/src/crates/assembly/core/src/agentic/execution/execution_engine.rs b/src/crates/assembly/core/src/agentic/execution/execution_engine.rs index 38b63afadc..a444458065 100644 --- a/src/crates/assembly/core/src/agentic/execution/execution_engine.rs +++ b/src/crates/assembly/core/src/agentic/execution/execution_engine.rs @@ -5885,7 +5885,7 @@ mod tests { session_id: "session".to_string(), dialog_turn_id: "turn".to_string(), turn_index: 0, - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace: Some(workspace), context: HashMap::new(), subagent_parent_info: None, @@ -6468,7 +6468,7 @@ mod tests { session_id: "session".to_string(), dialog_turn_id: "turn".to_string(), turn_index: 0, - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace: None, context: HashMap::new(), subagent_parent_info: None, diff --git a/src/crates/assembly/core/src/agentic/execution/round_executor.rs b/src/crates/assembly/core/src/agentic/execution/round_executor.rs index 11c0ace1a7..1fc09f5c45 100644 --- a/src/crates/assembly/core/src/agentic/execution/round_executor.rs +++ b/src/crates/assembly/core/src/agentic/execution/round_executor.rs @@ -1761,7 +1761,7 @@ mod tests { primary_model_facts: tool_runtime::context::PrimaryModelFacts::new( "model-1", "model-1", "openai", true, ), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), context_vars: HashMap::new(), permission_constraints: Default::default(), permission_runtime_ceiling: None, diff --git a/src/crates/assembly/core/src/agentic/fork_agent/mod.rs b/src/crates/assembly/core/src/agentic/fork_agent/mod.rs index 8f7ba5fac3..c74bb24dd7 100644 --- a/src/crates/assembly/core/src/agentic/fork_agent/mod.rs +++ b/src/crates/assembly/core/src/agentic/fork_agent/mod.rs @@ -86,7 +86,7 @@ mod tests { max_turns: 42, ..SessionConfig::default() }; - Session::new("Parent".to_string(), "agentic".to_string(), config) + Session::new("Parent".to_string(), "Standard".to_string(), config) } #[test] @@ -149,7 +149,7 @@ mod tests { #[test] fn snapshot_retains_parent_agent_type_state() { let mut parent = parent_session(); - parent.last_user_dialog_agent_type = Some("agentic".to_string()); + parent.last_user_dialog_agent_type = Some("Standard".to_string()); parent.last_submitted_agent_type = Some("Cowork".to_string()); let snapshot = @@ -157,7 +157,7 @@ mod tests { assert_eq!( snapshot.last_user_dialog_agent_type.as_deref(), - Some("agentic") + Some("Standard") ); assert_eq!( snapshot.last_submitted_agent_type.as_deref(), diff --git a/src/crates/assembly/core/src/agentic/insights/collector.rs b/src/crates/assembly/core/src/agentic/insights/collector.rs index a9910235ba..8618752161 100644 --- a/src/crates/assembly/core/src/agentic/insights/collector.rs +++ b/src/crates/assembly/core/src/agentic/insights/collector.rs @@ -1078,7 +1078,7 @@ mod tests { turn_id.to_string(), turn_index, "session-1".to_string(), - Some("Agentic".to_string()), + Some("Standard".to_string()), UserMessageData { id: format!("user-{turn_id}"), content: format!("message-{turn_id}"), @@ -1219,7 +1219,7 @@ mod tests { let mut parent = SessionMetadata::new( "parent".to_string(), "Parent".to_string(), - "Agentic".to_string(), + "Standard".to_string(), "model".to_string(), ); parent.last_active_at = 1_000; diff --git a/src/crates/assembly/core/src/agentic/insights/facet_cache.rs b/src/crates/assembly/core/src/agentic/insights/facet_cache.rs index dbde96cadd..d7e4219f1f 100644 --- a/src/crates/assembly/core/src/agentic/insights/facet_cache.rs +++ b/src/crates/assembly/core/src/agentic/insights/facet_cache.rs @@ -97,7 +97,7 @@ mod tests { fn transcript(workspace_path: &str) -> SessionTranscript { SessionTranscript { session_id: "same-session".to_string(), - agent_type: "Agentic".to_string(), + agent_type: "Standard".to_string(), session_name: "Session".to_string(), workspace_path: Some(workspace_path.to_string()), last_activity_unix_secs: 10, diff --git a/src/crates/assembly/core/src/agentic/memories/service.rs b/src/crates/assembly/core/src/agentic/memories/service.rs index 3cd00317f1..9d1a3049ba 100644 --- a/src/crates/assembly/core/src/agentic/memories/service.rs +++ b/src/crates/assembly/core/src/agentic/memories/service.rs @@ -1123,7 +1123,7 @@ mod tests { let mut metadata = SessionMetadata::new( "btw-session".to_string(), "Side thread".to_string(), - "agentic".to_string(), + "Standard".to_string(), "model".to_string(), ); metadata.session_kind = SessionKind::Standard; diff --git a/src/crates/assembly/core/src/agentic/memories/startup.rs b/src/crates/assembly/core/src/agentic/memories/startup.rs index 46252d10b5..6619638109 100644 --- a/src/crates/assembly/core/src/agentic/memories/startup.rs +++ b/src/crates/assembly/core/src/agentic/memories/startup.rs @@ -134,7 +134,7 @@ mod tests { MemoryStartupRequest { session_id: "session-a".to_string(), session_kind: SessionKind::Standard, - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some("workspace".to_string()), is_remote_workspace: false, has_user_input: true, diff --git a/src/crates/assembly/core/src/agentic/persistence/manager.rs b/src/crates/assembly/core/src/agentic/persistence/manager.rs index 319325c973..6637435fb5 100644 --- a/src/crates/assembly/core/src/agentic/persistence/manager.rs +++ b/src/crates/assembly/core/src/agentic/persistence/manager.rs @@ -2588,7 +2588,7 @@ impl PersistenceManager { session_id: metadata.session_id.clone(), session_name: metadata.session_name.clone(), agent_type: if legacy_minimal { - "minimal".to_string() + "Minimal".to_string() } else { metadata.agent_type.clone() }, @@ -2959,7 +2959,7 @@ impl PersistenceManager { session_id: metadata.session_id, session_name: metadata.session_name, agent_type: if legacy_minimal { - "minimal".to_string() + "Minimal".to_string() } else { metadata.agent_type }, @@ -4674,7 +4674,7 @@ mod tests { &SessionMetadata::new( session_id.to_string(), "Hidden suffix".to_string(), - "agentic".to_string(), + "Standard".to_string(), "model-a".to_string(), ), ) @@ -4785,7 +4785,7 @@ mod tests { let session = Session::new_with_id( format!("model-summary-{}", Uuid::new_v4()), "Model summary".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), model_id: Some("fast".to_string()), @@ -5509,7 +5509,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Catalog persistence".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -5589,7 +5589,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Catalog restore".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -5680,7 +5680,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Legacy catalog window".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -5770,7 +5770,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Catalog missing file".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -5825,7 +5825,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Incremental catalog repair".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -5934,7 +5934,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Catalog rebuild fallback".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -6003,7 +6003,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Catalog staged revert".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -6078,7 +6078,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Turn window".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -6374,7 +6374,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Concurrent metadata".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -6526,7 +6526,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Transactional deletion".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -6598,7 +6598,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Transactional session deletion".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -6644,7 +6644,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Canonical metadata lock".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -7584,7 +7584,7 @@ mod tests { let historical = Session::new_with_id( historical_id.clone(), "Historical remote session".to_string(), - "agentic".to_string(), + "Standard".to_string(), config.clone(), ); manager @@ -7645,7 +7645,7 @@ mod tests { let new_session = Session::new_with_id( new_session_id.clone(), "New remote session".to_string(), - "agentic".to_string(), + "Standard".to_string(), config, ); manager diff --git a/src/crates/assembly/core/src/agentic/persistence/session_branch.rs b/src/crates/assembly/core/src/agentic/persistence/session_branch.rs index 32a1646cd7..19738268e4 100644 --- a/src/crates/assembly/core/src/agentic/persistence/session_branch.rs +++ b/src/crates/assembly/core/src/agentic/persistence/session_branch.rs @@ -368,7 +368,7 @@ mod tests { let workspace = TestWorkspace::new(); let manager = PersistenceManager::new(workspace.path_manager()).unwrap(); - let source = Session::new("Source".into(), "agentic".into(), Default::default()); + let source = Session::new("Source".into(), "Standard".into(), Default::default()); manager .save_session(workspace.path(), &source) .await @@ -531,7 +531,7 @@ mod tests { let mut source_session = Session::new( "Source Title".to_string(), - "agentic".to_string(), + "Standard".to_string(), Default::default(), ); source_session.kind = SessionKind::Standard; @@ -658,7 +658,7 @@ mod tests { assert_ne!(result.session_id, source_session.session_id); assert_eq!(result.session_name, "Source Title (1)"); - assert_eq!(result.agent_type, "agentic"); + assert_eq!(result.agent_type, "Standard"); let branched_session = manager .load_session(workspace.path(), &result.session_id) .await @@ -763,7 +763,7 @@ mod tests { PersistenceManager::new(workspace.path_manager()).expect("persistence manager"); let source_session = Session::new( "Source Title".to_string(), - "agentic".to_string(), + "Standard".to_string(), Default::default(), ); manager @@ -821,7 +821,7 @@ mod tests { PersistenceManager::new(workspace.path_manager()).expect("persistence manager"); let mut source_session = Session::new( "Source Title".to_string(), - "agentic".to_string(), + "Standard".to_string(), Default::default(), ); source_session.compression_state.compression_count = 3; @@ -905,7 +905,7 @@ mod tests { PersistenceManager::new(workspace.path_manager()).expect("persistence manager"); let mut source_session = Session::new( "Source Title".to_string(), - "agentic".to_string(), + "Standard".to_string(), Default::default(), ); source_session.kind = SessionKind::Standard; @@ -984,7 +984,7 @@ mod tests { PersistenceManager::new(workspace.path_manager()).expect("persistence manager"); let mut source_session = Session::new( "Source Title".to_string(), - "agentic".to_string(), + "Standard".to_string(), Default::default(), ); source_session.kind = SessionKind::Standard; @@ -1065,7 +1065,7 @@ mod tests { ); let mut source_session = Session::new( "Source Title".to_string(), - "agentic".to_string(), + "Standard".to_string(), Default::default(), ); source_session.kind = SessionKind::Standard; @@ -1083,7 +1083,7 @@ mod tests { let mut second_source_session = Session::new( "Source Title".to_string(), - "agentic".to_string(), + "Standard".to_string(), Default::default(), ); second_source_session.kind = SessionKind::Standard; @@ -1143,7 +1143,7 @@ mod tests { let mut source_session = Session::new( "Source Title".to_string(), - "agentic".to_string(), + "Standard".to_string(), Default::default(), ); source_session.kind = SessionKind::Standard; diff --git a/src/crates/assembly/core/src/agentic/session/revert.rs b/src/crates/assembly/core/src/agentic/session/revert.rs index c4df1ed0bf..751ff52923 100644 --- a/src/crates/assembly/core/src/agentic/session/revert.rs +++ b/src/crates/assembly/core/src/agentic/session/revert.rs @@ -233,7 +233,7 @@ mod tests { session_id: "session-1".to_string(), timestamp: index as u64, kind: DialogTurnKind::UserDialog, - agent_type: Some("agentic".to_string()), + agent_type: Some("Standard".to_string()), user_message: UserMessageData { id: format!("message-{index}"), content: prompt.to_string(), diff --git a/src/crates/assembly/core/src/agentic/session/session_manager.rs b/src/crates/assembly/core/src/agentic/session/session_manager.rs index a3b88c12ba..81a366eef9 100644 --- a/src/crates/assembly/core/src/agentic/session/session_manager.rs +++ b/src/crates/assembly/core/src/agentic/session/session_manager.rs @@ -5980,7 +5980,7 @@ impl SessionManager { } else { let fallback_mode = available_modes .iter() - .find(|mode| mode.id == "agentic") + .find(|mode| mode.id == "Standard") .or_else(|| available_modes.first()) .map(|mode| mode.id.clone()) .ok_or_else(|| { @@ -9871,19 +9871,19 @@ mod tests { let expired_at = now - Duration::from_secs(120); let mut durable = Session::new( "Durable".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig::default(), ); durable.last_activity_at = expired_at; let mut transient = Session::new( "Connection scoped".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig::default(), ); transient.last_activity_at = expired_at; let mut processing = Session::new( "Processing".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig::default(), ); processing.last_activity_at = expired_at; @@ -9933,7 +9933,7 @@ mod tests { let expired_at = now - Duration::from_secs(120); let mut session = Session::new( "Becomes active".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig::default(), ); session.last_activity_at = expired_at; @@ -10129,7 +10129,7 @@ mod tests { let session = manager .create_session( "Completion merge".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..SessionConfig::default() @@ -10140,7 +10140,7 @@ mod tests { let turn_id = manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "finish the response".to_string(), Some("turn-completion-prefix".to_string()), None, @@ -10310,7 +10310,7 @@ mod tests { let session = manager .create_session( "Usage persistence".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..SessionConfig::default() @@ -10356,7 +10356,7 @@ mod tests { let session = manager .create_session( "Recovery".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..SessionConfig::default() @@ -10367,7 +10367,7 @@ mod tests { let turn_id = manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "finish the task".to_string(), Some("turn-1".to_string()), None, @@ -10469,7 +10469,7 @@ mod tests { let session = manager .create_session( "Recovery permission".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), permission_mode: Some(PermissionMode::FullAccess), @@ -10481,7 +10481,7 @@ mod tests { let turn_id = manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "finish with the original permission".to_string(), Some("turn-permission".to_string()), None, @@ -10529,7 +10529,7 @@ mod tests { let session = manager .create_session( "Recovery reasoning preset".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), reasoning_preset: Some("high".to_string()), @@ -10541,7 +10541,7 @@ mod tests { let turn_id = manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "continue with one reasoning contract".to_string(), Some("turn-reasoning".to_string()), None, @@ -10592,7 +10592,7 @@ mod tests { let session = manager .create_session( "Recovery auto reasoning".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { model_id: Some("model-original".to_string()), workspace_path: Some(workspace.path().to_string_lossy().to_string()), @@ -10604,7 +10604,7 @@ mod tests { let turn_id = manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "continue with the original auto reasoning default".to_string(), Some("turn-auto-reasoning".to_string()), None, @@ -10658,7 +10658,7 @@ mod tests { let session = manager .create_session( "Recovery model binding".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { model_id: Some("model-original".to_string()), workspace_path: Some(workspace.path().to_string_lossy().to_string()), @@ -10670,7 +10670,7 @@ mod tests { let turn_id = manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "continue with the original model binding".to_string(), Some("turn-model-binding".to_string()), None, @@ -10724,7 +10724,7 @@ mod tests { let session = manager .create_session( "Turn admission CAS".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), model_id: Some("model-original".to_string()), @@ -10745,7 +10745,7 @@ mod tests { let error = manager .start_dialog_turn_with_prepended_messages_if_session_matches( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "must retry admission".to_string(), Some("turn-admission-race".to_string()), None, @@ -10770,7 +10770,7 @@ mod tests { let session = manager .create_session( "Turn admission context window CAS".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), model_id: Some("model-original".to_string()), @@ -10795,7 +10795,7 @@ mod tests { let error = manager .start_dialog_turn_with_prepended_messages_if_session_matches( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "must reject stale context window".to_string(), Some("turn-admission-context-window-race".to_string()), None, @@ -10820,7 +10820,7 @@ mod tests { let session = manager .create_session( "Turn admission route CAS".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..SessionConfig::default() @@ -10832,7 +10832,7 @@ mod tests { manager .update_session_agent_binding( &session.session_id, - "agentic", + "Standard", SessionAgentRouteOwner::External, None, ) @@ -10842,7 +10842,7 @@ mod tests { let error = manager .start_dialog_turn_with_prepended_messages_if_session_matches( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "must reject stale route owner".to_string(), Some("turn-admission-route-race".to_string()), None, @@ -10867,7 +10867,7 @@ mod tests { let session = manager .create_session( "Turn admission route key CAS".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), agent_route_key: Some("local:agentic:v1".to_string()), @@ -10880,7 +10880,7 @@ mod tests { manager .update_session_agent_binding( &session.session_id, - "agentic", + "Standard", SessionAgentRouteOwner::Local, Some("local:agentic:v2".to_string()), ) @@ -10890,7 +10890,7 @@ mod tests { let error = manager .start_dialog_turn_with_prepended_messages_if_session_matches( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "must reject stale route key".to_string(), Some("turn-admission-route-key-race".to_string()), None, @@ -10916,7 +10916,7 @@ mod tests { let session = manager .create_session( "Turn admission workspace CAS".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(original_workspace.clone()), project_workspace_path: Some(original_workspace.clone()), @@ -10949,7 +10949,7 @@ mod tests { let error = manager .start_dialog_turn_with_prepended_messages_if_session_matches( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "must reject stale workspace binding".to_string(), Some("turn-admission-workspace-race".to_string()), None, @@ -10974,7 +10974,7 @@ mod tests { let session = manager .create_session( "Recovery rollback".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..SessionConfig::default() @@ -10985,7 +10985,7 @@ mod tests { let turn_id = manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "finish the task".to_string(), Some("turn-rollback".to_string()), None, @@ -11107,7 +11107,7 @@ mod tests { let session = manager .create_session( "Recovery append".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..SessionConfig::default() @@ -11118,7 +11118,7 @@ mod tests { let turn_id = manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "finish the task".to_string(), Some("turn-append".to_string()), None, @@ -11226,7 +11226,7 @@ mod tests { let session = manager .create_session( "Repeated recovery".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..SessionConfig::default() @@ -11237,7 +11237,7 @@ mod tests { let turn_id = manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "continue through two interruptions".to_string(), Some("turn-repeat-interruption".to_string()), None, @@ -11348,7 +11348,7 @@ mod tests { let session = manager .create_session( "Interrupted merge".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..SessionConfig::default() @@ -11359,7 +11359,7 @@ mod tests { let turn_id = manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "merge frontend and runtime rounds".to_string(), Some("turn-prefix-merge".to_string()), None, @@ -11432,7 +11432,7 @@ mod tests { let session = manager .create_session( "Recovery restart".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { model_id: Some("primary".to_string()), workspace_path: Some(workspace.path().to_string_lossy().to_string()), @@ -11444,7 +11444,7 @@ mod tests { let turn_id = manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "finish after restart".to_string(), Some("turn-restart".to_string()), None, @@ -11568,7 +11568,7 @@ mod tests { let session = manager .create_session( "Delayed usage".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..SessionConfig::default() @@ -11634,7 +11634,7 @@ mod tests { let session = manager .create_session( "Binding race".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..SessionConfig::default() @@ -11681,7 +11681,7 @@ mod tests { let session = manager .create_session( "View-only binding".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), project_workspace_path: Some(workspace.path().to_string_lossy().to_string()), @@ -11734,7 +11734,7 @@ mod tests { let session = manager .create_session( "Boundary zero binding".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), project_workspace_path: Some(workspace.path().to_string_lossy().to_string()), @@ -11845,7 +11845,7 @@ mod tests { let first = manager .create_session( "First loaded session".to_string(), - "agentic".to_string(), + "Standard".to_string(), config.clone(), ) .await @@ -11868,7 +11868,7 @@ mod tests { let second = manager .create_session( "Second loaded session".to_string(), - "agentic".to_string(), + "Standard".to_string(), config, ) .await @@ -11889,7 +11889,7 @@ mod tests { let session = first .create_session( "Single writer".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -11902,7 +11902,7 @@ mod tests { .create_session_with_id( Some(session.session_id.clone()), "Duplicate".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -11956,13 +11956,13 @@ mod tests { let first_session = Session::new_with_id( "first-workspace-session".to_string(), "First".to_string(), - "agentic".to_string(), + "Standard".to_string(), config.clone(), ); let second_session = Session::new_with_id( "second-workspace-session".to_string(), "Second".to_string(), - "agentic".to_string(), + "Standard".to_string(), config, ); persistence @@ -12003,7 +12003,7 @@ mod tests { let session = first .create_session( "Aliased workspace".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -12040,7 +12040,7 @@ mod tests { let fixture = Session::new_with_id( session_id.to_string(), "Recovered fixture".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -12074,7 +12074,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Failed fixture".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -12090,7 +12090,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Recovered fixture".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -12114,7 +12114,7 @@ mod tests { let session = first .create_session( "Unload failure".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -12151,7 +12151,7 @@ mod tests { let session = first .create_session( "Processing".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -12194,7 +12194,7 @@ mod tests { .create_session_with_id_and_details( None, "First".to_string(), - "agentic".to_string(), + "Standard".to_string(), config.clone(), None, SessionKind::Standard, @@ -12205,7 +12205,7 @@ mod tests { .create_session_with_id_and_details( None, "Second".to_string(), - "agentic".to_string(), + "Standard".to_string(), config, None, SessionKind::Standard, @@ -12257,7 +12257,7 @@ mod tests { let mut session = Session::new_with_id( session_id.clone(), "Active permission session".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig::default(), ); session.state = SessionState::Processing { @@ -12323,7 +12323,7 @@ mod tests { .create_transient_session_with_id_and_details( None, "Connection Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -12351,7 +12351,7 @@ mod tests { .create_transient_session_with_id_and_details( None, "Prepared Subagent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -12383,12 +12383,12 @@ mod tests { }; let first = Session::new( "First persisted".to_string(), - "agentic".to_string(), + "Standard".to_string(), config.clone(), ); let second = Session::new( "Second persisted".to_string(), - "agentic".to_string(), + "Standard".to_string(), config, ); persistence_manager @@ -12455,7 +12455,7 @@ mod tests { .create_session_with_id( Some("capacity-first".to_string()), "First".to_string(), - "agentic".to_string(), + "Standard".to_string(), config, ) .await @@ -12468,7 +12468,7 @@ mod tests { .create_session_with_id( Some("capacity-second".to_string()), "Second".to_string(), - "agentic".to_string(), + "Standard".to_string(), config, ) .await @@ -12576,7 +12576,7 @@ mod tests { let session = manager .create_session( "Persisted model update".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), model_id: Some("primary".to_string()), @@ -12609,7 +12609,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Legacy model selector".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), model_id: Some("auto".to_string()), @@ -12658,7 +12658,7 @@ mod tests { let session = manager .create_session( "Failed model update".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), model_id: Some("primary".to_string()), @@ -12756,7 +12756,7 @@ mod tests { .create_session_with_id_and_details( Some(session_id.to_string()), "Original".to_string(), - "agentic".to_string(), + "Standard".to_string(), config.clone(), None, SessionKind::EphemeralChild, @@ -12767,7 +12767,7 @@ mod tests { .create_session_with_id_and_details( Some(session_id.to_string()), "Duplicate".to_string(), - "agentic".to_string(), + "Standard".to_string(), config.clone(), None, SessionKind::EphemeralChild, @@ -12781,7 +12781,7 @@ mod tests { .create_session_with_id_and_details( Some(session_id.to_string()), "Recreated".to_string(), - "agentic".to_string(), + "Standard".to_string(), config, None, SessionKind::EphemeralChild, @@ -12810,7 +12810,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Original".to_string(), - "agentic".to_string(), + "Standard".to_string(), config.clone(), ) .await @@ -12825,7 +12825,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Replacement".to_string(), - "agentic".to_string(), + "Standard".to_string(), config, ) .await @@ -12849,7 +12849,7 @@ mod tests { .create_session_with_id( Some(invalid_id.to_string()), "Invalid".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -12881,7 +12881,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Must not become visible".to_string(), - "agentic".to_string(), + "Standard".to_string(), config.clone(), ) .await @@ -12902,7 +12902,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Retry succeeds".to_string(), - "agentic".to_string(), + "Standard".to_string(), config, ) .await @@ -12922,7 +12922,7 @@ mod tests { let session = manager .create_session( "Original".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -12975,7 +12975,7 @@ mod tests { let session = manager .create_session( "Original".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -13012,7 +13012,7 @@ mod tests { let session = manager .create_session( "Original".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), ..Default::default() @@ -13045,7 +13045,7 @@ mod tests { let session = manager .create_session( "Original".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), ..Default::default() @@ -13090,7 +13090,7 @@ mod tests { let session = manager .create_session( "Original".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), ..Default::default() @@ -13127,7 +13127,7 @@ mod tests { let session = manager .create_session( "Active".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -13170,7 +13170,7 @@ mod tests { let session = manager .create_session( "Active".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -13213,7 +13213,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Serialized".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path), ..Default::default() @@ -13241,7 +13241,7 @@ mod tests { let session = manager .create_session( "Persisted".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -13280,7 +13280,7 @@ mod tests { let session = manager .create_session( "Serialized mode update".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -13317,7 +13317,7 @@ mod tests { let session = manager .create_session( "Serialized compression update".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -13360,7 +13360,7 @@ mod tests { let session = manager .create_session( "Serialized turn start".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -13376,7 +13376,7 @@ mod tests { manager_for_turn .start_dialog_turn( &session_id, - "agentic".to_string(), + "Standard".to_string(), "hello".to_string(), Some("serialized-turn".to_string()), None, @@ -13404,7 +13404,7 @@ mod tests { let session = manager .create_session( "Idempotent mode update".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -13421,7 +13421,7 @@ mod tests { tokio::time::sleep(Duration::from_millis(20)).await; manager - .update_session_agent_type(&session.session_id, "agentic") + .update_session_agent_type(&session.session_id, "Standard") .await .expect("same mode should succeed"); @@ -13442,7 +13442,7 @@ mod tests { let session = manager .create_session( "Durable mode update".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -13480,7 +13480,7 @@ mod tests { let session = manager .create_session( "Durable external route".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -13492,7 +13492,7 @@ mod tests { manager .update_session_agent_binding( &session.session_id, - "agentic", + "Standard", SessionAgentRouteOwner::External, Some("test:external:agentic".to_string()), ) @@ -13509,7 +13509,7 @@ mod tests { manager .update_session_agent_binding( &session.session_id, - "agentic", + "Standard", SessionAgentRouteOwner::Local, Some("local:agentic".to_string()), ) @@ -13567,7 +13567,7 @@ mod tests { let session = manager .create_session( "Metadata-only mode update".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -13671,7 +13671,7 @@ mod tests { let mut session = Session::new_with_id( "reasoning-selection".to_string(), "Reasoning selection".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { model_id: Some("model-a".to_string()), reasoning_preset: Some("high".to_string()), @@ -13701,7 +13701,7 @@ mod tests { let session = manager .create_session( "Stale reasoning preset".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().into_owned()), model_id: Some("model-b".to_string()), @@ -13768,7 +13768,7 @@ mod tests { manager .create_session( "Remote session".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), model_id: Some("deepseek-v4-flash".to_string()), @@ -13799,7 +13799,7 @@ mod tests { let mut session = Session::new_with_id( "session-804".to_string(), "DeepSeek session".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { model_id: Some("deepseek-v4-pro".to_string()), max_context_tokens: 256_000, @@ -13829,7 +13829,7 @@ mod tests { let mut session = Session::new_with_id( "session-default".to_string(), "Default session".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { model_id: None, max_context_tokens: 256_000, @@ -14204,7 +14204,7 @@ mod tests { .create_session_with_id_and_details( Some(Uuid::new_v4().to_string()), "Side thread".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -14241,7 +14241,7 @@ mod tests { .create_session_with_id_and_details( Some(Uuid::new_v4().to_string()), "Main thread".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -14597,7 +14597,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Resolved sessions restore".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -14629,7 +14629,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Resolved sessions restore".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -14663,7 +14663,7 @@ mod tests { let session = Session::new_with_id( session_id.clone(), "Remote identity restore".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some("/home/wsp/project".to_string()), remote_connection_id: Some("ssh-1".to_string()), @@ -14763,7 +14763,7 @@ mod tests { let session = manager .create_session( "Fork child".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -14783,7 +14783,7 @@ mod tests { let turn_id = manager .start_dialog_turn_with_existing_context( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "delegate task".to_string(), Some("subagent-turn-0".to_string()), None, @@ -14841,7 +14841,7 @@ mod tests { let session = manager .create_session( "Atomic admission".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -14852,7 +14852,7 @@ mod tests { let dialog = manager.start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "new user input".to_string(), Some("dialog-turn".to_string()), None, @@ -14897,7 +14897,7 @@ mod tests { let session = manager .create_session( "Retry admission".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -14932,7 +14932,7 @@ mod tests { let retry_turn = manager .start_dialog_turn( &session.session_id, - "agentic".to_string(), + "Standard".to_string(), "retry after failure".to_string(), Some("retry-turn".to_string()), None, @@ -14952,7 +14952,7 @@ mod tests { let session = manager .create_session( "Failed maintenance".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -15246,7 +15246,7 @@ mod tests { }, ); turn.agent_type = Some(if index == 0 { - "agentic".to_string() + "Standard".to_string() } else { "Cowork".to_string() }); @@ -15315,7 +15315,7 @@ mod tests { .expect("turns should load"); assert_eq!(turns.len(), 1); assert_eq!(turns[0].user_message.content, "prompt 0"); - assert_eq!(turns[0].agent_type.as_deref(), Some("agentic")); + assert_eq!(turns[0].agent_type.as_deref(), Some("Standard")); assert!(persistence_manager .load_turn_context_snapshot(workspace.path(), &session.session_id, 1) .await @@ -15341,7 +15341,7 @@ mod tests { assert_eq!(restored.dialog_turn_ids, vec!["turn-0".to_string()]); assert_eq!( restored.last_user_dialog_agent_type.as_deref(), - Some("agentic") + Some("Standard") ); assert_eq!( manager @@ -15380,7 +15380,7 @@ mod tests { let session = manager .create_session( "Staged revert".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -15734,7 +15734,7 @@ mod tests { let session = manager .create_session( "Listing baseline rebuild".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -15848,7 +15848,7 @@ mod tests { let mut session = Session::new_with_id( session_id.clone(), "Restore sanitize".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -15951,7 +15951,7 @@ mod tests { let session = manager .create_session( "Rollback sanitize".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -16480,7 +16480,7 @@ mod tests { let session = manager .create_session( "Durable evidence".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -16584,7 +16584,7 @@ mod tests { let session = manager .create_session( "Staged evidence".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -16664,7 +16664,7 @@ mod tests { let session = manager .create_session( "Staged revert evidence".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -17109,7 +17109,7 @@ mod tests { let session = writer .create_session( "Staged revert explicit history evidence".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -17248,7 +17248,7 @@ mod tests { let session = manager .create_session( "Rollback evidence".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -17385,7 +17385,7 @@ mod tests { let session = manager .create_session( "Evidence write failure".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -17425,7 +17425,7 @@ mod tests { let session = manager .create_session( "Concurrent evidence".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -17506,7 +17506,7 @@ mod tests { let session = manager .create_session( "Corrupt evidence".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -17550,7 +17550,7 @@ mod tests { let session = manager .create_session( "Corrupt retention evidence".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -17589,7 +17589,7 @@ mod tests { let session = manager .create_session( "Remote evidence".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some("/home/wsp/project".to_string()), remote_connection_id: Some("ssh-1".to_string()), @@ -17644,7 +17644,7 @@ mod tests { let session = manager .create_session( "Converge evidence".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -17764,7 +17764,7 @@ mod tests { let session = manager .create_session( "Fork evidence".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -17882,7 +17882,7 @@ mod tests { let session = manager .create_session( "Prompt cache".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path), ..Default::default() @@ -17940,7 +17940,7 @@ mod tests { let session = manager .create_session( "Listing baseline".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -18098,7 +18098,7 @@ mod tests { let session = manager .create_session( "Edit constraint persistence".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -18223,7 +18223,7 @@ mod tests { let parent = manager .create_session( "Parent".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -18234,7 +18234,7 @@ mod tests { let child = manager .create_session( "Child".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -18324,7 +18324,7 @@ mod tests { let session = manager .create_session( "Prompt cache".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path), ..Default::default() @@ -18393,7 +18393,7 @@ mod tests { let session = manager .create_session( "Prompt cache".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -18476,7 +18476,7 @@ mod tests { let source_session = manager .create_session( "Prompt cache source".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path.clone()), ..Default::default() @@ -18487,7 +18487,7 @@ mod tests { let target_session = manager .create_session( "Prompt cache target".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path), ..Default::default() @@ -18568,7 +18568,7 @@ mod tests { let session = manager .create_session( "Prompt cache".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { workspace_path: Some(workspace_path), ..Default::default() diff --git a/src/crates/assembly/core/src/agentic/session/transcript_render.rs b/src/crates/assembly/core/src/agentic/session/transcript_render.rs index 01a9de3040..ac3ef2d532 100644 --- a/src/crates/assembly/core/src/agentic/session/transcript_render.rs +++ b/src/crates/assembly/core/src/agentic/session/transcript_render.rs @@ -632,7 +632,7 @@ mod search_projection_tests { session_id: "session_1".to_string(), timestamp: 1, kind: DialogTurnKind::UserDialog, - agent_type: Some("agentic".to_string()), + agent_type: Some("Standard".to_string()), user_message: UserMessageData { id: "user_1".to_string(), content: "question".to_string(), diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/agent_delete_tool.rs b/src/crates/assembly/core/src/agentic/tools/implementations/agent_delete_tool.rs index 3beadc763e..20c9cb7339 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/agent_delete_tool.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/agent_delete_tool.rs @@ -217,7 +217,7 @@ mod tests { let intents = AgentDeleteTool::new() .permission_intents( &serde_json::json!({ "agent_ids": ["a2", "a3"] }), - &context("Ultra"), + &context("Ultimate"), ) .expect("permission intent"); assert_eq!(intents.len(), 1); diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/agent_list_tool.rs b/src/crates/assembly/core/src/agentic/tools/implementations/agent_list_tool.rs index 97f60a2703..fcf0506f59 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/agent_list_tool.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/agent_list_tool.rs @@ -167,7 +167,7 @@ mod tests { async fn validation_accepts_only_swarm_planners() { assert!( AgentListTool::new() - .validate_input(&serde_json::json!({}), Some(&context("Ultra"))) + .validate_input(&serde_json::json!({}), Some(&context("Ultimate"))) .await .result ); diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/exec_command/command.rs b/src/crates/assembly/core/src/agentic/tools/implementations/exec_command/command.rs index 9b51c5054c..2d3e28db5a 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/exec_command/command.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/exec_command/command.rs @@ -1048,7 +1048,7 @@ mod tests { fn local_tool_context(workspace: &Path) -> ToolUseContext { ToolUseContext { tool_call_id: None, - agent_type: Some("agentic".to_string()), + agent_type: Some("Standard".to_string()), session_id: None, dialog_turn_id: None, workspace: Some(WorkspaceBinding::new(None, workspace.to_path_buf())), @@ -1067,7 +1067,7 @@ mod tests { .expect("remote session identity should build"); ToolUseContext { tool_call_id: None, - agent_type: Some("agentic".to_string()), + agent_type: Some("Standard".to_string()), session_id: None, dialog_turn_id: None, workspace: Some(WorkspaceBinding::new_remote( @@ -1355,7 +1355,7 @@ mod tests { let tool = ExecCommandTool::new(); let context = ToolUseContext { tool_call_id: None, - agent_type: Some("agentic".to_string()), + agent_type: Some("Standard".to_string()), session_id: None, dialog_turn_id: None, workspace: None, diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/frontend_workbench_tool.rs b/src/crates/assembly/core/src/agentic/tools/implementations/frontend_workbench_tool.rs index 76ccf3972f..f275b38dfc 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/frontend_workbench_tool.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/frontend_workbench_tool.rs @@ -234,7 +234,7 @@ mod tests { #[tokio::test] async fn execution_rejects_non_creative_agents_even_if_called_directly() { let error = FrontendWorkbenchTool::new() - .call_impl(&json!({"action": "status"}), &context("agentic")) + .call_impl(&json!({"action": "status"}), &context("Standard")) .await .expect_err("non-Creative call must fail"); assert!(error.to_string().contains("Creative mode")); diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/session_control_tool.rs b/src/crates/assembly/core/src/agentic/tools/implementations/session_control_tool.rs index 8dd3b85a6f..583a121de7 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/session_control_tool.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/session_control_tool.rs @@ -294,8 +294,8 @@ Actions: Arguments: - "workspace": Absolute workspace path. Required for create and list. Ignored for cancel, delete, and rename. - "session_name": Used by create (defaults to "New Session") and required as the new title for rename. -- "agent_type": Only used by create. Defaults to "agentic". - - "agentic": Coding-focused agent for implementation, debugging, and code changes. +- "agent_type": Only used by create. Defaults to "Standard". + - "Standard": Coding-focused agent for implementation, debugging, and code changes. - "Cowork": Collaborative agent for office-style work such as research, documentation, presentations, etc. - "DeepResearch": Research agent for systematic investigation and evidence-driven reports. - "session_id": Required for cancel, delete, and rename."# @@ -334,7 +334,7 @@ Arguments: }, "agent_type": { "type": "string", - "enum": ["agentic", "Cowork", "DeepResearch"], + "enum": ["Standard", "Cowork", "DeepResearch"], "description": "Optional agent type when creating a session. Defaults to agentic." } }, diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/session_message_tool.rs b/src/crates/assembly/core/src/agentic/tools/implementations/session_message_tool.rs index 2e842b2585..532a1b0362 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/session_message_tool.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/session_message_tool.rs @@ -272,7 +272,7 @@ impl SessionMessageTool { #[derive(Debug, Clone, Deserialize)] enum SessionMessageAgentType { - #[serde(rename = "agentic", alias = "Agentic", alias = "AGENTIC")] + #[serde(rename = "Standard", alias = "Standard", alias = "AGENTIC")] Agentic, #[serde(rename = "Cowork", alias = "cowork", alias = "COWORK")] Cowork, @@ -287,7 +287,7 @@ enum SessionMessageAgentType { impl SessionMessageAgentType { fn as_str(&self) -> &'static str { match self { - Self::Agentic => "agentic", + Self::Agentic => "Standard", Self::Cowork => "Cowork", Self::DeepResearch => "DeepResearch", } @@ -318,7 +318,7 @@ Usage: - Reusing an existing session: provide "session_id" and "message". You may omit "workspace"; the tool will resolve it from the target session when possible. Allowed agent types when creating a session: -- "agentic": Coding-focused agent for implementation, debugging, and code changes. +- "Standard": Coding-focused agent for implementation, debugging, and code changes. - "Cowork": Collaborative agent for office-style work such as research, documentation, presentations, etc. - "DeepResearch": Research agent for systematic investigation and evidence-driven reports. "# @@ -356,7 +356,7 @@ Allowed agent types when creating a session: }, "agent_type": { "type": "string", - "enum": ["agentic", "Cowork", "DeepResearch"], + "enum": ["Standard", "Cowork", "DeepResearch"], "description": "Required when session_id is omitted. Not allowed when sending to an existing session." } }, @@ -905,9 +905,9 @@ mod tests { #[test] fn target_agent_type_uses_resolved_agent_type() { assert_eq!( - SessionMessageTool::target_agent_type_from_resolution(Some("agentic".to_string())) + SessionMessageTool::target_agent_type_from_resolution(Some("Standard".to_string())) .as_deref(), - Some("agentic") + Some("Standard") ); } @@ -916,7 +916,7 @@ mod tests { let sessions = vec![AgentSessionSummary { session_id: "worker_1".to_string(), session_name: "Worker".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), model_id: None, reasoning_preset: None, last_user_dialog_agent_type: None, @@ -928,7 +928,7 @@ mod tests { assert_eq!( SessionMessageTool::target_agent_type_from_sessions(&sessions, "worker_1").as_deref(), - Some("agentic") + Some("Standard") ); } @@ -987,7 +987,7 @@ mod tests { &json!({ "workspace": workspace.as_string(), "message": "hello", - "agent_type": "agentic", + "agent_type": "Standard", }), Some(&session_context("source_1")), ) @@ -1069,7 +1069,7 @@ mod tests { &json!({ "message": "hello", "session_name": "Worker Session", - "agent_type": "agentic", + "agent_type": "Standard", }), Some(&session_context("source_1")), ) diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/skill_tool.rs b/src/crates/assembly/core/src/agentic/tools/implementations/skill_tool.rs index ba44330ce4..ddcade56a7 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/skill_tool.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/skill_tool.rs @@ -876,7 +876,7 @@ Use the remote project skill. ); let context = crate::agentic::tools::framework::ToolUseContext { tool_call_id: None, - agent_type: Some("agentic".to_string()), + agent_type: Some("Standard".to_string()), session_id: None, dialog_turn_id: None, workspace: Some(workspace), diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/skills/policy.rs b/src/crates/assembly/core/src/agentic/tools/implementations/skills/policy.rs index 00bc085b7e..328e1c1803 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/skills/policy.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/skills/policy.rs @@ -11,7 +11,7 @@ mod tests { #[test] fn builtin_defaults_follow_mode_policies() { assert_eq!( - resolve_builtin_default_enabled("ppt-design", "agentic"), + resolve_builtin_default_enabled("ppt-design", "Standard"), Some(false) ); assert_eq!( @@ -19,7 +19,7 @@ mod tests { Some(true) ); assert_eq!( - resolve_builtin_default_enabled("agent-browser", "agentic"), + resolve_builtin_default_enabled("agent-browser", "Standard"), Some(false) ); assert_eq!( @@ -43,11 +43,11 @@ mod tests { Some(false) ); assert_eq!( - resolve_builtin_default_enabled("agent-browser", "coding_shared"), + resolve_builtin_default_enabled("agent-browser", "Standard"), Some(false) ); assert_eq!( - resolve_builtin_default_enabled("ppt-design", "coding_shared"), + resolve_builtin_default_enabled("ppt-design", "Standard"), Some(false) ); assert_eq!( @@ -55,12 +55,15 @@ mod tests { Some(false) ); assert_eq!( - resolve_builtin_default_enabled("agent-browser", "Ultra"), + resolve_builtin_default_enabled("agent-browser", "Ultimate"), Some(true) ); - assert_eq!(resolve_builtin_default_enabled("plan", "Ultra"), Some(true)); assert_eq!( - resolve_builtin_default_enabled("find-skills", "Ultra"), + resolve_builtin_default_enabled("plan", "Ultimate"), + Some(true) + ); + assert_eq!( + resolve_builtin_default_enabled("find-skills", "Ultimate"), Some(false) ); assert_eq!( @@ -71,7 +74,7 @@ mod tests { resolve_builtin_default_enabled("plan", "SwarmWorker"), Some(false) ); - for mode_id in ["agentic", "Claw", "Creative", "Cowork", "DeepResearch"] { + for mode_id in ["Standard", "Claw", "Creative", "Cowork", "DeepResearch"] { assert_eq!( resolve_builtin_default_enabled("openbitfun-canvas", mode_id), Some(false), @@ -82,6 +85,9 @@ mod tests { #[test] fn unknown_builtins_return_none() { - assert_eq!(resolve_builtin_default_enabled("not-real", "agentic"), None); + assert_eq!( + resolve_builtin_default_enabled("not-real", "Standard"), + None + ); } } diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/skills/resolver.rs b/src/crates/assembly/core/src/agentic/tools/implementations/skills/resolver.rs index 34896bfe5b..8c0bdac376 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/skills/resolver.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/skills/resolver.rs @@ -69,11 +69,13 @@ mod tests { assert!(!resolve_skill_default_enabled_for_mode( &presentation, - "agentic" + "Standard" )); // Agentic and Cowork use ControlHub's browser domain by default, so // agent-browser remains opt-in for those modes. - assert!(!resolve_skill_default_enabled_for_mode(&browser, "agentic")); + assert!(!resolve_skill_default_enabled_for_mode( + &browser, "Standard" + )); assert!(resolve_skill_default_enabled_for_mode( &presentation, "Cowork" @@ -86,7 +88,7 @@ mod tests { let custom = custom_user_skill("my-custom-skill"); let state = resolve_skill_state_for_mode( &custom, - "agentic", + "Standard", &UserModeSkillOverrides::default(), &HashSet::new(), ); @@ -103,7 +105,7 @@ mod tests { let disabled_project = HashSet::new(); let disabled_state = - resolve_skill_state_for_mode(&presentation, "agentic", &overrides, &disabled_project); + resolve_skill_state_for_mode(&presentation, "Standard", &overrides, &disabled_project); assert!(!disabled_state.effective_enabled); assert_eq!( disabled_state.reason, @@ -112,7 +114,7 @@ mod tests { overrides.enabled_skills.push(presentation.key.clone()); let enabled_state = - resolve_skill_state_for_mode(&presentation, "agentic", &overrides, &disabled_project); + resolve_skill_state_for_mode(&presentation, "Standard", &overrides, &disabled_project); assert!(enabled_state.effective_enabled); assert_eq!( enabled_state.reason, @@ -127,13 +129,13 @@ mod tests { let disabled_project = HashSet::new(); let default_state = - resolve_skill_state_for_mode(&canvas, "agentic", &overrides, &disabled_project); + resolve_skill_state_for_mode(&canvas, "Standard", &overrides, &disabled_project); assert!(!default_state.default_enabled); assert!(!default_state.effective_enabled); overrides.enabled_skills.push(canvas.key.clone()); let enabled_state = - resolve_skill_state_for_mode(&canvas, "agentic", &overrides, &disabled_project); + resolve_skill_state_for_mode(&canvas, "Standard", &overrides, &disabled_project); assert!(!enabled_state.default_enabled); assert!(enabled_state.effective_enabled); assert_eq!( diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/task/tests.rs b/src/crates/assembly/core/src/agentic/tools/implementations/task/tests.rs index b7062be28e..fe8cf43621 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/task/tests.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/task/tests.rs @@ -888,7 +888,7 @@ async fn validate_input_infers_send_input_without_action_when_agent_id_present() #[test] fn permission_intents_follow_the_agent_id_contract() { let tool = TaskTool::new(); - let context = test_tool_context("agentic"); + let context = test_tool_context("Standard"); for (action, expected_resource) in [("send_input", "send_input:a1"), ("cancel", "cancel:a1")] { let intents = tool @@ -929,7 +929,7 @@ async fn validate_input_rejects_send_input_with_subagent_type() { #[tokio::test] async fn validate_input_rejects_deep_review_retry_fields_for_regular_parent() { - let context = test_tool_context("agentic"); + let context = test_tool_context("Standard"); let validation = TaskTool::new() .validate_input( &json!({ @@ -952,7 +952,7 @@ async fn validate_input_rejects_deep_review_retry_fields_for_regular_parent() { #[tokio::test] async fn validate_input_rejects_timeout_for_regular_parent() { - let context = test_tool_context("agentic"); + let context = test_tool_context("Standard"); let validation = TaskTool::new() .validate_input( &json!({ @@ -1128,7 +1128,7 @@ async fn call_impl_rejects_nested_subagent_delegation() { let policy = DelegationPolicy::top_level().spawn_child(); let context = ToolUseContext { tool_call_id: Some("tool-call-1".to_string()), - agent_type: Some("agentic".to_string()), + agent_type: Some("Standard".to_string()), session_id: Some("session-1".to_string()), dialog_turn_id: Some("turn-1".to_string()), workspace: None, @@ -1273,7 +1273,7 @@ fn deep_review_cancelled_reviewer_result_tells_parent_not_to_relaunch() { async fn description_with_context_filters_restricted_subagents_by_parent_agent() { let agentic_context = ToolUseContext { tool_call_id: None, - agent_type: Some("agentic".to_string()), + agent_type: Some("Standard".to_string()), session_id: None, dialog_turn_id: None, workspace: None, @@ -1308,7 +1308,7 @@ async fn description_with_context_filters_restricted_subagents_by_parent_agent() #[tokio::test] async fn swarm_planners_use_static_agent_types_without_dynamic_listing() { - for parent_agent_type in ["Ultra", "SwarmPlanner"] { + for parent_agent_type in ["Ultimate", "SwarmPlanner"] { let context = test_tool_context(parent_agent_type); assert_eq!( TaskTool::build_available_agents_context_section(Some(&context)).await, @@ -1330,7 +1330,7 @@ async fn swarm_planners_use_static_agent_types_without_dynamic_listing() { async fn prompt_stability_description_with_context_renders_available_agents_in_stable_order() { let context = ToolUseContext { tool_call_id: None, - agent_type: Some("agentic".to_string()), + agent_type: Some("Standard".to_string()), session_id: None, dialog_turn_id: None, workspace: None, diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/worktree_tool.rs b/src/crates/assembly/core/src/agentic/tools/implementations/worktree_tool.rs index f8fae0951e..6734cbb373 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/worktree_tool.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/worktree_tool.rs @@ -223,7 +223,7 @@ The tool cannot remove or rebind the worktree in which it is running. Use Sessio }, "agent_type": { "type": "string", - "enum": ["agentic", "Cowork"], + "enum": ["Standard", "Cowork"], "description": "Optional mode for create_session. Defaults to agentic." } }, @@ -518,7 +518,7 @@ The tool cannot remove or rebind the worktree in which it is running. Use Sessio session_name: input .session_name .unwrap_or_else(|| "New Worktree Session".to_string()), - agent_type: input.agent_type.unwrap_or_else(|| "agentic".to_string()), + agent_type: input.agent_type.unwrap_or_else(|| "Standard".to_string()), agent_route_key: None, workspace_path: Some(created.execution_target.root_path.clone()), project_workspace_path: Some(project_workspace_path.clone()), diff --git a/src/crates/assembly/core/src/agentic/tools/pipeline/state_manager.rs b/src/crates/assembly/core/src/agentic/tools/pipeline/state_manager.rs index 4f10f8907d..14acc3c576 100644 --- a/src/crates/assembly/core/src/agentic/tools/pipeline/state_manager.rs +++ b/src/crates/assembly/core/src/agentic/tools/pipeline/state_manager.rs @@ -343,7 +343,7 @@ mod tests { round_id: "round-1".to_string(), attempt_id: None, attempt_index: None, - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace: None, primary_model_facts: tool_runtime::context::PrimaryModelFacts::default(), context_vars: HashMap::new(), diff --git a/src/crates/assembly/core/src/agentic/tools/plugin_host_tool.rs b/src/crates/assembly/core/src/agentic/tools/plugin_host_tool.rs index 82e4724e6f..962ba09934 100644 --- a/src/crates/assembly/core/src/agentic/tools/plugin_host_tool.rs +++ b/src/crates/assembly/core/src/agentic/tools/plugin_host_tool.rs @@ -741,7 +741,7 @@ mod tests { PluginToolExecutionRoute { session_id: "session-a".to_string(), dialog_turn_id: "turn-a".to_string(), - agent: "agentic".to_string(), + agent: "Standard".to_string(), tool_name: "plugin-tool".to_string(), tool_call_id: Some("tool-call-a".to_string()), generation_key: "generation-a".to_string(), @@ -905,7 +905,7 @@ mod tests { ))) .is_none()); assert!(mux - .route_for(Some(&context(&workspace_text, "Agentic"))) + .route_for(Some(&context(&workspace_text, "Standard"))) .is_none()); registry.clear_source_workspace(RuntimeHookSource::Plugin, &scope); @@ -930,7 +930,7 @@ mod tests { mux.set_route(scope.clone(), route); let workspace_text = workspace.to_string_lossy(); - let context = context(&workspace_text, "Agentic"); + let context = context(&workspace_text, "Standard"); assert!(mux.route_for(Some(&context)).is_some()); registry.clear_source_workspace(RuntimeHookSource::Plugin, &scope); @@ -955,7 +955,7 @@ mod tests { mux.set_route(scope.clone(), route); let workspace_text = workspace.to_string_lossy(); - let mut remote = context(&workspace_text, "Agentic"); + let mut remote = context(&workspace_text, "Standard"); remote.workspace.as_mut().expect("workspace").backend = crate::agentic::workspace::WorkspaceBackend::Remote { connection_id: "remote-a".to_string(), @@ -1014,7 +1014,7 @@ mod tests { PluginToolExecutionRoute { session_id: "session-a".to_string(), dialog_turn_id: "turn-a".to_string(), - agent: "agentic".to_string(), + agent: "Standard".to_string(), tool_name: "plugin-tool".to_string(), tool_call_id: Some("tool-call-a".to_string()), generation_key: "generation-a".to_string(), diff --git a/src/crates/assembly/core/src/agentic/tools/product_runtime/catalog.rs b/src/crates/assembly/core/src/agentic/tools/product_runtime/catalog.rs index 6964b830bc..faf7ca8ba5 100644 --- a/src/crates/assembly/core/src/agentic/tools/product_runtime/catalog.rs +++ b/src/crates/assembly/core/src/agentic/tools/product_runtime/catalog.rs @@ -588,7 +588,7 @@ mod tests { append_selected_plugin_tool_names( &mut allowed_tools, &selected, - &tool_context(Some("Agentic")), + &tool_context(Some("Standard")), ) .await; @@ -699,7 +699,7 @@ mod tests { let manifest = resolve_product_tool_manifest( &allowed_tools, &AgentToolPolicyOverrides::default(), - &tool_context(Some("agentic")), + &tool_context(Some("Standard")), ) .await; @@ -718,7 +718,7 @@ mod tests { #[tokio::test] async fn runtime_restrictions_hide_tools_from_manifest_and_get_tool_spec() { let allowed_tools = vec!["Read".to_string(), "WebFetch".to_string()]; - let mut context = tool_context(Some("agentic")); + let mut context = tool_context(Some("Standard")); context .runtime_tool_restrictions .denied_tool_names @@ -765,7 +765,7 @@ mod tests { let manifest = resolve_product_resolved_tool_manifest( &allowed_tools, &AgentToolPolicyOverrides::default(), - &tool_context(Some("agentic")), + &tool_context(Some("Standard")), ) .await; @@ -813,7 +813,7 @@ mod tests { .expect("CallDeferredTool gateway"), Arc::new(DeferredMcpCatalogTool) as Arc, ]; - let context = tool_context(Some("agentic")); + let context = tool_context(Some("Standard")); let manifest = openbitfun_agent_tools::resolve_contextual_tool_manifest( &tool_snapshot, &["mcp__github__search_repos".to_string()], @@ -888,7 +888,7 @@ mod tests { CALL_DEFERRED_TOOL_NAME.to_string(), "mcp__github__search_repos".to_string(), ]; - let mut context = tool_context(Some("agentic")); + let mut context = tool_context(Some("Standard")); context.custom_data.insert( DEFERRED_TOOL_LOADING_CONTEXT_KEY.to_string(), Value::String("false".to_string()), @@ -949,7 +949,7 @@ mod tests { let visible = resolve_product_resolved_visible_tools( &["Read".to_string(), "WebFetch".to_string()], &AgentToolPolicyOverrides::default(), - &tool_context(Some("agentic")), + &tool_context(Some("Standard")), ) .await; @@ -1001,7 +1001,7 @@ mod tests { async fn product_get_tool_spec_returns_assistant_hint_for_direct_webfetch_in_agentic_mode() { let results = resolve_product_get_tool_spec_results( &json!({ "tool_name": "WebFetch" }), - &tool_context(Some("agentic")), + &tool_context(Some("Standard")), "GetToolSpec", ) .await @@ -1032,12 +1032,12 @@ mod tests { #[tokio::test] async fn product_agentic_manifest_keeps_canvas_tools_opt_in() { let policy = crate::agentic::agents::get_agent_registry() - .get_agent_tool_policy("agentic", None) + .get_agent_tool_policy("Standard", None) .await; let manifest = resolve_product_resolved_tool_manifest( &policy.allowed_tools, &policy.exposure_overrides, - &tool_context(Some("agentic")), + &tool_context(Some("Standard")), ) .await; diff --git a/src/crates/assembly/core/src/agentic/tools/tool_context_runtime.rs b/src/crates/assembly/core/src/agentic/tools/tool_context_runtime.rs index 8537ad7543..1567a0f4ae 100644 --- a/src/crates/assembly/core/src/agentic/tools/tool_context_runtime.rs +++ b/src/crates/assembly/core/src/agentic/tools/tool_context_runtime.rs @@ -877,7 +877,7 @@ mod context_facts_tests { fn tool_context_facts_preserve_portable_fields_without_runtime_handles() { let context = ToolUseContext { tool_call_id: Some("call-1".to_string()), - agent_type: Some("Agentic".to_string()), + agent_type: Some("Standard".to_string()), session_id: Some("session-1".to_string()), dialog_turn_id: Some("turn-1".to_string()), workspace: Some(WorkspaceBinding::new(None, PathBuf::from("/repo/project"))), @@ -898,7 +898,7 @@ mod context_facts_tests { let facts = context.to_tool_context_facts(); assert_eq!(facts.tool_call_id.as_deref(), Some("call-1")); - assert_eq!(facts.agent_type.as_deref(), Some("Agentic")); + assert_eq!(facts.agent_type.as_deref(), Some("Standard")); assert_eq!(facts.session_id.as_deref(), Some("session-1")); assert_eq!(facts.dialog_turn_id.as_deref(), Some("turn-1")); assert_eq!(facts.workspace_kind, Some(ToolWorkspaceKind::Local)); @@ -925,7 +925,7 @@ mod context_facts_tests { let context = ToolUseContext { tool_call_id: Some("call-runtime".to_string()), - agent_type: Some("Agentic".to_string()), + agent_type: Some("Standard".to_string()), session_id: Some("session-runtime".to_string()), dialog_turn_id: Some("turn-runtime".to_string()), workspace: Some(WorkspaceBinding::new(None, PathBuf::from("/repo/runtime"))), diff --git a/src/crates/assembly/core/src/external_tools.rs b/src/crates/assembly/core/src/external_tools.rs index 477bbec313..b1f842c11f 100644 --- a/src/crates/assembly/core/src/external_tools.rs +++ b/src/crates/assembly/core/src/external_tools.rs @@ -2706,7 +2706,7 @@ mod tests { )); for runtime_agent_key in [ - "Agentic", + "Standard", "Plan", "external_subagent_runtime:other-provider:agent", "external_subagent_runtime:opencode:agent", @@ -2746,7 +2746,7 @@ mod tests { .insert(mux.name.clone(), mux.clone()); let registered: Arc = mux; - let context = local_tool_context(&workspace, "Agentic"); + let context = local_tool_context(&workspace, "Standard"); assert!(Arc::ptr_eq( &router .resolve_registered_tool_for_context(registered, &context) @@ -2782,7 +2782,7 @@ mod tests { .expect("router lock") .insert(mux.name.clone(), Arc::new(mux)); - let context = local_tool_context(&workspace, "Agentic"); + let context = local_tool_context(&workspace, "Standard"); assert!(Arc::ptr_eq( &router .resolve_registered_tool_for_context(original, &context) @@ -2822,7 +2822,7 @@ mod tests { router.workspace_routes(&workspace_key).get(&tool_name), Some(WorkspaceRoute::Original { conflict: None }) )); - let context = local_tool_context(&workspace, "Agentic"); + let context = local_tool_context(&workspace, "Standard"); assert!(Arc::ptr_eq( &mux.selected(Some(&context)).expect("native route"), &original diff --git a/src/crates/assembly/core/src/plugin_capability_publication.rs b/src/crates/assembly/core/src/plugin_capability_publication.rs index d24a3d92cf..eeb6565b69 100644 --- a/src/crates/assembly/core/src/plugin_capability_publication.rs +++ b/src/crates/assembly/core/src/plugin_capability_publication.rs @@ -1,5 +1,5 @@ use crate::agentic::agents::{ - external_subagent_runtime_key, get_agent_registry, shared_coding_mode_tools, ExploreAgent, + external_subagent_runtime_key, get_agent_registry, standard_harness_tools, ExploreAgent, ExternalProvidedAgent, ExternalSubagentModelBinding, ExternalSubagentRegistration, ExternalSubagentRoute, }; @@ -317,7 +317,7 @@ fn native_tool_baseline( use crate::agentic::agents::Agent; ExploreAgent::new().default_tools() } else { - shared_coding_mode_tools() + standard_harness_tools() } } diff --git a/src/crates/assembly/core/src/plugin_host_http_routes.rs b/src/crates/assembly/core/src/plugin_host_http_routes.rs index 174fe34e84..016d862f6c 100644 --- a/src/crates/assembly/core/src/plugin_host_http_routes.rs +++ b/src/crates/assembly/core/src/plugin_host_http_routes.rs @@ -769,7 +769,7 @@ mod tests { session_id: "session-a".to_string(), timestamp: 10, kind: Default::default(), - agent_type: Some("agentic".to_string()), + agent_type: Some("Standard".to_string()), user_message: crate::service::session::UserMessageData { id: "user-message".to_string(), content: "hello".to_string(), diff --git a/src/crates/assembly/core/src/plugin_host_http_routes_impl.rs b/src/crates/assembly/core/src/plugin_host_http_routes_impl.rs index 8b8b00c82e..d148b62917 100644 --- a/src/crates/assembly/core/src/plugin_host_http_routes_impl.rs +++ b/src/crates/assembly/core/src/plugin_host_http_routes_impl.rs @@ -122,7 +122,7 @@ async fn session_create(context: &PluginHostInstance, body: &[u8]) -> RouteResul input .title .unwrap_or_else(|| "OpenCode Plugin Session".to_string()), - "agentic".to_string(), + "Standard".to_string(), crate::agentic::core::SessionConfig { workspace_path: Some(context.directory.to_string_lossy().into_owned()), project_workspace_path: Some(context.directory.to_string_lossy().into_owned()), @@ -959,10 +959,11 @@ async fn file_read( } async fn file_status(context: &PluginHostInstance) -> RouteResult { - let snapshot = openbitfun_services_integrations::git::GitWorkspaceDiffPort::new(&context.directory) - .workspace_diff() - .await - .map_err(|error| Failure::backend(error.to_string()))?; + let snapshot = + openbitfun_services_integrations::git::GitWorkspaceDiffPort::new(&context.directory) + .workspace_diff() + .await + .map_err(|error| Failure::backend(error.to_string()))?; Ok(Value::Array(snapshot.files.into_iter().map(|file| json!({ "path": file.path, "added": file.additions, diff --git a/src/crates/assembly/core/src/product_runtime.rs b/src/crates/assembly/core/src/product_runtime.rs index 3d937f790b..a3221445b3 100644 --- a/src/crates/assembly/core/src/product_runtime.rs +++ b/src/crates/assembly/core/src/product_runtime.rs @@ -3080,7 +3080,7 @@ mod tests { let root = SessionMetadata::new( "root".to_string(), "Root".to_string(), - "agentic".to_string(), + "Standard".to_string(), "model".to_string(), ); let mut child = SessionMetadata::new( @@ -3257,7 +3257,7 @@ mod tests { let root = SessionMetadata::new( "root".to_string(), "Root".to_string(), - "agentic".to_string(), + "Standard".to_string(), "model".to_string(), ); let mut child = SessionMetadata::new( @@ -3434,7 +3434,7 @@ mod tests { .create_session_with_id( Some(session_id.to_string()), "Reload test".to_string(), - "agentic".to_string(), + "Standard".to_string(), crate::agentic::core::SessionConfig { workspace_path: Some(workspace.path().to_string_lossy().to_string()), ..Default::default() @@ -3706,7 +3706,7 @@ mod tests { let source = crate::agentic::core::Session::new_with_id( source_session_id.clone(), format!("Remote fork {host}"), - "agentic".to_string(), + "Standard".to_string(), crate::agentic::core::SessionConfig { workspace_path: Some(remote_path.clone()), remote_connection_id: Some(connection_id.to_string()), @@ -3997,7 +3997,7 @@ mod tests { let source = crate::agentic::core::Session::new_with_id( "remote-source".into(), "Remote source".into(), - "agentic".into(), + "Standard".into(), crate::agentic::core::SessionConfig { workspace_path: Some("/workspace/repo".into()), remote_connection_id: Some("ssh-source".into()), @@ -4120,7 +4120,7 @@ mod tests { let mut metadata = SessionMetadata::new( session_id.to_string(), "Latest fork".to_string(), - "agentic".to_string(), + "Standard".to_string(), "model-a".to_string(), ); metadata.workspace_path = Some(workspace_root.to_string_lossy().into_owned()); @@ -4208,7 +4208,7 @@ mod tests { .create_transient_session_with_id_and_details( Some("session-transient-settlement".to_string()), "Transient settlement".to_string(), - "agentic".to_string(), + "Standard".to_string(), crate::agentic::core::SessionConfig { workspace_path: Some(workspace_root.to_string_lossy().into_owned()), ..Default::default() @@ -4221,7 +4221,7 @@ mod tests { let transient_turn_id = session_manager .start_dialog_turn( &transient.session_id, - "agentic".to_string(), + "Standard".to_string(), "finish transiently".to_string(), Some("turn-transient-settlement".to_string()), None, @@ -4255,7 +4255,7 @@ mod tests { ); assert_ne!(result.session_id, session_id); - assert_eq!(result.agent_type, "agentic"); + assert_eq!(result.agent_type, "Standard"); assert_eq!( persistence .load_session_turns(&storage_path, &result.session_id) @@ -4483,7 +4483,7 @@ mod tests { &SessionMetadata::new( session_id.to_string(), "Usage session".to_string(), - "agentic".to_string(), + "Standard".to_string(), "model-a".to_string(), ), ) @@ -4659,7 +4659,7 @@ mod tests { &SessionMetadata::new( runtime_session_id.to_string(), "Runtime owned".to_string(), - "agentic".to_string(), + "Standard".to_string(), "model-a".to_string(), ), ) diff --git a/src/crates/assembly/core/src/service/config/agent_profile_project_store.rs b/src/crates/assembly/core/src/service/config/agent_profile_project_store.rs index 957d6e7079..5b0e71ad8e 100644 --- a/src/crates/assembly/core/src/service/config/agent_profile_project_store.rs +++ b/src/crates/assembly/core/src/service/config/agent_profile_project_store.rs @@ -13,6 +13,8 @@ pub const PROJECT_AGENT_PROFILES_FILE_NAME: &str = "agent_profiles.json"; pub struct ProjectAgentProfileSkillConfig { #[serde(skip_serializing_if = "Vec::is_empty")] pub disabled_project_skills: Vec, + #[serde(flatten)] + pub extensions: serde_json::Map, } #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] @@ -20,6 +22,8 @@ pub struct ProjectAgentProfileSkillConfig { pub struct ProjectAgentProfileSubagentConfig { #[serde(skip_serializing_if = "HashMap::is_empty")] pub overrides: ParentSubagentOverrideConfig, + #[serde(flatten)] + pub extensions: serde_json::Map, } #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] @@ -29,25 +33,27 @@ pub struct ProjectAgentProfileEntry { pub skills: ProjectAgentProfileSkillConfig, #[serde(skip_serializing_if = "ProjectAgentProfileSubagentConfig::is_empty")] pub subagents: ProjectAgentProfileSubagentConfig, + #[serde(flatten)] + pub extensions: serde_json::Map, } pub type ProjectAgentProfilesDocument = HashMap; impl ProjectAgentProfileSkillConfig { fn is_empty(&self) -> bool { - self.disabled_project_skills.is_empty() + self.disabled_project_skills.is_empty() && self.extensions.is_empty() } } impl ProjectAgentProfileSubagentConfig { fn is_empty(&self) -> bool { - self.overrides.is_empty() + self.overrides.is_empty() && self.extensions.is_empty() } } impl ProjectAgentProfileEntry { fn is_empty(&self) -> bool { - self.skills.is_empty() && self.subagents.is_empty() + self.skills.is_empty() && self.subagents.is_empty() && self.extensions.is_empty() } } @@ -108,8 +114,14 @@ pub fn normalize_project_agent_profiles_document( pub fn deserialize_project_agent_profiles_document( content: &str, ) -> OpenBitFunResult { + let raw = serde_json::from_str(content)?; + let migrated = + openbitfun_config_contracts::agent_identity_migration::canonicalize_agent_profile_keys( + &raw, + ) + .map_err(OpenBitFunError::config)?; Ok(normalize_project_agent_profiles_document( - serde_json::from_str(content)?, + serde_json::from_value(serde_json::Value::Object(migrated))?, )) } @@ -117,7 +129,7 @@ pub fn serialize_project_agent_profiles_document( document: &ProjectAgentProfilesDocument, ) -> OpenBitFunResult> { Ok(serde_json::to_vec_pretty( - &normalize_project_agent_profiles_document(document.clone()), + &deserialize_project_agent_profiles_document(&serde_json::to_string(document)?)?, )?) } @@ -256,3 +268,52 @@ pub fn set_project_subagent_override_state( } set_project_subagent_overrides(document, profile_id, overrides) } + +#[cfg(test)] +mod identity_migration_tests { + use super::*; + + #[test] + fn legacy_project_overrides_survive_canonical_round_trip() { + let old = r#"{"coding_shared":{"skills":{"disabled_project_skills":["project::docs"]}},"Ultra":{"subagents":{"overrides":{"SwarmWorker":"disabled"}}}}"#; + let document = deserialize_project_agent_profiles_document(old).unwrap(); + assert_eq!( + get_disabled_project_skills(&document, "Standard"), + vec!["project::docs"] + ); + assert!(get_project_subagent_overrides(&document, "Ultimate").contains_key("SwarmWorker")); + let bytes = serialize_project_agent_profiles_document(&document).unwrap(); + let written = String::from_utf8(bytes).unwrap(); + assert!(!written.contains("coding_shared")); + assert!(!written.contains("Ultra")); + assert_eq!( + deserialize_project_agent_profiles_document(&written).unwrap(), + document + ); + } + + #[test] + fn ambiguous_project_aliases_are_reported_without_resetting_config() { + let old = r#"{"agentic":{"skills":{"disabled_project_skills":["a"]}},"Standard":{"skills":{"disabled_project_skills":["b"]}}}"#; + assert!(deserialize_project_agent_profiles_document(old).is_err()); + } + + #[test] + fn migration_and_skill_reset_preserve_unknown_project_fields() { + let old = r#"{"coding_shared":{"future_policy":true,"skills":{"disabled_project_skills":["project::docs"],"future_skill_policy":false},"subagents":{"future_delegation_policy":["x"]}}}"#; + let mut document = deserialize_project_agent_profiles_document(old).unwrap(); + set_disabled_project_skills(&mut document, "Standard", Vec::new()); + let written: serde_json::Value = + serde_json::from_slice(&serialize_project_agent_profiles_document(&document).unwrap()) + .unwrap(); + assert_eq!(written["Standard"]["future_policy"], true); + assert_eq!(written["Standard"]["skills"]["future_skill_policy"], false); + assert_eq!( + written["Standard"]["subagents"]["future_delegation_policy"], + serde_json::json!(["x"]) + ); + assert!(written["Standard"]["skills"] + .get("disabled_project_skills") + .is_none()); + } +} diff --git a/src/crates/assembly/core/src/service/config/mode_config_canonicalizer.rs b/src/crates/assembly/core/src/service/config/mode_config_canonicalizer.rs index 2d84237a5c..7ce2870323 100644 --- a/src/crates/assembly/core/src/service/config/mode_config_canonicalizer.rs +++ b/src/crates/assembly/core/src/service/config/mode_config_canonicalizer.rs @@ -231,9 +231,26 @@ fn stored_agent_profile_from_overrides( enabled_user_skills, subagent_overrides, tool_permission_rules, + ..Default::default() }) } +fn retain_profile_extensions( + profile_id: &str, + canonical: Option, + extensions: Map, +) -> Option { + if extensions.is_empty() { + return canonical; + } + let mut config = canonical.unwrap_or_else(|| AgentProfileConfig { + profile_id: profile_id.to_string(), + ..Default::default() + }); + config.extensions = extensions; + Some(config) +} + fn build_agent_profile_view( agent_id: &str, default_tools: Vec, @@ -284,18 +301,21 @@ fn canonicalize_agent_profile( stored.profile_id = profile_id.to_string(); } - Ok(stored_agent_profile_from_overrides( - StoredAgentProfileOverrides { - agent_id: profile_id, - added_tools: stored.added_tools, - removed_tools: stored.removed_tools, - disabled_user_skills: stored.disabled_user_skills, - enabled_user_skills: stored.enabled_user_skills, - subagent_overrides: stored.subagent_overrides, - tool_permission_rules: stored.tool_permission_rules, - default_tools, - valid_tools, - }, + let canonical = stored_agent_profile_from_overrides(StoredAgentProfileOverrides { + agent_id: profile_id, + added_tools: stored.added_tools, + removed_tools: stored.removed_tools, + disabled_user_skills: stored.disabled_user_skills, + enabled_user_skills: stored.enabled_user_skills, + subagent_overrides: stored.subagent_overrides, + tool_permission_rules: stored.tool_permission_rules, + default_tools, + valid_tools, + }); + Ok(retain_profile_extensions( + profile_id, + canonical, + stored.extensions, )) } @@ -343,10 +363,19 @@ async fn get_profile_defaults() -> HashMap> { pub async fn get_agent_profile_configs() -> OpenBitFunResult> { let config_service = GlobalConfigManager::get_service().await?; - Ok(config_service - .get_config(Some("ai.agent_profiles")) - .await - .unwrap_or_default()) + let raw = config_service.get_config(Some("ai.agent_profiles")).await?; + let mut migrated = + openbitfun_config_contracts::agent_identity_migration::canonicalize_agent_profile_keys( + &raw, + ) + .map_err(OpenBitFunError::config)?; + migrated.retain(|_, value| !value.is_null()); + let mut profiles: HashMap = + serde_json::from_value(Value::Object(migrated))?; + for (id, config) in &mut profiles { + config.profile_id = id.clone(); + } + Ok(profiles) } pub async fn get_agent_profile_views() -> OpenBitFunResult> { @@ -370,6 +399,7 @@ pub async fn get_agent_profile_views() -> OpenBitFunResult OpenBitFunResult { + let agent_id = openbitfun_core_types::agent_identity::canonical_agent_id(agent_id); let views = get_agent_profile_views().await?; views .get(agent_id) @@ -381,6 +411,7 @@ pub async fn persist_agent_profile_from_value( agent_id: &str, config: Value, ) -> OpenBitFunResult<()> { + let agent_id = openbitfun_core_types::agent_identity::canonical_agent_id(agent_id); let config_service = GlobalConfigManager::get_service().await?; let agent_defaults = get_agent_defaults().await; let default_tools = agent_defaults @@ -491,7 +522,10 @@ pub async fn persist_agent_profile_from_value( .unwrap_or_default() }; - if let Some(canonical) = stored_agent_profile_from_tool_selection( + let extensions = current + .map(|config| config.extensions.clone()) + .unwrap_or_default(); + let canonical = stored_agent_profile_from_tool_selection( agent_id, enabled_tools, disabled_user_skills, @@ -500,7 +534,10 @@ pub async fn persist_agent_profile_from_value( tool_permission_rules, default_tools, &valid_tools, - ) { + ); + if let Some(canonical) = + retain_profile_extensions(&profile_id, canonical, extensions) + { stored_configs.insert(profile_id, canonical); } else { stored_configs.remove(&profile_id); @@ -528,6 +565,7 @@ pub async fn reset_agent_profile_to_default(agent_id: &str) -> OpenBitFunResult< && current.enabled_user_skills.is_empty() && current.subagent_overrides.is_empty() && current.tool_permission_rules.is_empty() + && current.extensions.is_empty() { stored_configs.remove(&profile_id); } @@ -549,12 +587,14 @@ pub async fn canonicalize_agent_profile_configs( .update_config( "ai.agent_profiles", |raw_agent_profiles: &mut Map| { + let migrated = openbitfun_config_contracts::agent_identity_migration::canonicalize_agent_profile_keys(raw_agent_profiles) + .map_err(OpenBitFunError::config)?; let mut rewritten_agent_profiles = Map::new(); let mut updated_profiles = Vec::new(); let mut removed_profile_configs = Vec::new(); for (profile_id, default_tools) in &profile_defaults { - let raw_profile = raw_agent_profiles.get(profile_id); + let raw_profile = migrated.get(profile_id); let canonical = canonicalize_agent_profile( profile_id, raw_profile, @@ -579,19 +619,12 @@ pub async fn canonicalize_agent_profile_configs( // Profiles we cannot resolve defaults for are kept, not dropped. Canonicalization // runs at startup with no workspace, so project-scoped sub-agents are invisible // here; pruning them would silently discard the user's stored selection every - // launch. Records that no longer deserialize are still removed, so one bad entry - // cannot take the whole map down when it is read back. - for (profile_id, raw_profile) in raw_agent_profiles.iter() { + // launch. Unknown or unreadable records must survive an upgrade unchanged. + for (profile_id, raw_profile) in migrated.iter() { if profile_defaults.contains_key(profile_id) { continue; } - match serde_json::from_value::(raw_profile.clone()) { - Ok(config) => { - rewritten_agent_profiles - .insert(profile_id.clone(), serde_json::to_value(config)?); - } - Err(_) => removed_profile_configs.push(profile_id.clone()), - } + rewritten_agent_profiles.insert(profile_id.clone(), raw_profile.clone()); } *raw_agent_profiles = rewritten_agent_profiles; @@ -631,6 +664,26 @@ mod tests { use serde_json::Value; use std::collections::HashSet; + #[test] + fn canonicalization_retains_future_fields_even_without_known_overrides() { + for raw in [ + serde_json::json!({"profile_id": "Standard", "future_policy": {"enabled": false}}), + serde_json::json!({"profile_id": "Standard", "removed_tools": ["Bash"], "future_policy": ["x"]}), + ] { + let config = canonicalize_agent_profile( + "Standard", + Some(&raw), + &["Bash".into()], + &HashSet::from(["Bash".into()]), + ) + .unwrap() + .unwrap(); + let saved = serde_json::to_value(config).unwrap(); + assert_eq!(saved["future_policy"], raw["future_policy"]); + assert_eq!(saved["removed_tools"], raw["removed_tools"]); + } + } + /// Skill selection is stored per agent profile, so every agent whose default /// tools include `Skill` must resolve to a profile default. Otherwise saving /// its selection fails with "Agent does not exist" — which is what happened @@ -683,7 +736,7 @@ mod tests { fn stored_agent_profile_from_overrides_keeps_enabled_user_skills() { let valid_tools = HashSet::new(); let stored = stored_agent_profile_from_overrides(StoredAgentProfileOverrides { - agent_id: "agentic", + agent_id: "Standard", added_tools: Vec::new(), removed_tools: Vec::new(), disabled_user_skills: Vec::new(), @@ -695,7 +748,7 @@ mod tests { }) .expect("mode config should be retained when skill overrides exist"); - assert_eq!(stored.profile_id, "coding_shared"); + assert_eq!(stored.profile_id, "Standard"); assert_eq!( stored.enabled_user_skills, vec!["user::openbitfun-system::ppt-design".to_string()] @@ -712,7 +765,7 @@ mod tests { AgentSubagentOverrideState::Disabled, ); let stored = stored_agent_profile_from_overrides(StoredAgentProfileOverrides { - agent_id: "agentic", + agent_id: "Standard", added_tools: Vec::new(), removed_tools: Vec::new(), disabled_user_skills: Vec::new(), @@ -724,7 +777,7 @@ mod tests { }) .expect("mode config should be retained when subagent overrides exist"); - assert_eq!(stored.profile_id, "coding_shared"); + assert_eq!(stored.profile_id, "Standard"); assert_eq!(stored.subagent_overrides, subagent_overrides); } @@ -737,7 +790,7 @@ mod tests { PermissionEffect::Deny, )]; let stored = stored_agent_profile_from_overrides(StoredAgentProfileOverrides { - agent_id: "agentic", + agent_id: "Standard", added_tools: Vec::new(), removed_tools: Vec::new(), disabled_user_skills: Vec::new(), @@ -761,7 +814,7 @@ mod tests { "effect": "allow" }] }); - let canonical = canonicalize_agent_profile("agentic", Some(&raw), &[], &HashSet::new()) + let canonical = canonicalize_agent_profile("Standard", Some(&raw), &[], &HashSet::new()) .expect("profile should canonicalize") .expect("permission-only profile should be present"); @@ -781,11 +834,11 @@ mod tests { #[test] fn canonicalize_agent_profile_preserves_mcp_override_before_registration() { let raw = serde_json::json!({ - "profile_id": "coding_shared", + "profile_id": "Standard", "added_tools": ["mcp__github__list_issues", "missing_static_tool"] }); let canonical = canonicalize_agent_profile( - "coding_shared", + "Standard", Some(&raw), &["Read".to_string()], &HashSet::from(["Read".to_string()]), @@ -802,11 +855,11 @@ mod tests { #[test] fn canonicalize_agent_profile_preserves_explicit_canvas_selection() { let raw = serde_json::json!({ - "profile_id": "coding_shared", + "profile_id": "Standard", "added_tools": ["CreateCanvas"] }); let canonical = canonicalize_agent_profile( - "coding_shared", + "Standard", Some(&raw), &["Read".to_string()], &HashSet::from(["Read".to_string(), "CreateCanvas".to_string()]), @@ -821,8 +874,8 @@ mod tests { #[test] fn shared_modes_report_shared_profile_members() { assert_eq!( - agent_profile_member_mode_ids_for("agentic"), - vec!["agentic".to_string()] + agent_profile_member_mode_ids_for("Standard"), + vec!["Standard".to_string()] ); assert_eq!( agent_profile_member_mode_ids_for("Cowork"), diff --git a/src/crates/assembly/core/src/service/cron/service.rs b/src/crates/assembly/core/src/service/cron/service.rs index 9d8df095f2..c3fbb6c57d 100644 --- a/src/crates/assembly/core/src/service/cron/service.rs +++ b/src/crates/assembly/core/src/service/cron/service.rs @@ -854,7 +854,7 @@ fn materialize_launch_spec(launch: CronLaunchSpec) -> CronLaunchSpec { fn normalize_agent_type(agent_type: &str) -> String { if agent_type.trim().is_empty() { - "agentic".to_string() + "Standard".to_string() } else { agent_type.trim().to_string() } diff --git a/src/crates/assembly/core/src/service/cron/types.rs b/src/crates/assembly/core/src/service/cron/types.rs index d3e43e8151..5d4fb92f59 100644 --- a/src/crates/assembly/core/src/service/cron/types.rs +++ b/src/crates/assembly/core/src/service/cron/types.rs @@ -201,5 +201,5 @@ const fn default_enabled() -> bool { } fn default_agent_type() -> String { - "agentic".to_string() + "Standard".to_string() } diff --git a/src/crates/assembly/core/src/service/dispatch/mod.rs b/src/crates/assembly/core/src/service/dispatch/mod.rs index 7257a0e66f..804b2449d2 100644 --- a/src/crates/assembly/core/src/service/dispatch/mod.rs +++ b/src/crates/assembly/core/src/service/dispatch/mod.rs @@ -79,6 +79,9 @@ struct DispatchTargetJobEntry { workspace_path: String, title: String, #[serde(default)] + #[serde( + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_optional_agent_id" + )] agent_type: Option, #[serde(default)] approval_policy: Option, @@ -104,6 +107,9 @@ pub struct OutboundDispatchRecord { #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde( + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_optional_agent_id" + )] pub agent_type: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub approval_policy: Option, @@ -1021,7 +1027,7 @@ mod tests { .expect("record") .with_submission_metadata( Some("Reasoning job".to_string()), - "agentic".to_string(), + "Standard".to_string(), "remote".to_string(), Some("target-model".to_string()), Some("high".to_string()), diff --git a/src/crates/assembly/core/src/service/i18n/generated_locale_contract.rs b/src/crates/assembly/core/src/service/i18n/generated_locale_contract.rs index 718619dbd9..b372ad4e3b 100644 --- a/src/crates/assembly/core/src/service/i18n/generated_locale_contract.rs +++ b/src/crates/assembly/core/src/service/i18n/generated_locale_contract.rs @@ -69,24 +69,49 @@ pub const GENERATED_LOCALE_CONTRACT: &[GeneratedLocaleContractEntry] = &[ pub const GENERATED_SHARED_TERMS: &[GeneratedSharedTermEntry] = &[ GeneratedSharedTermEntry { locale: LocaleId::ZhCN, - key: "agents.claw", - value: "Claw", + key: "agents.Claw", + value: "助理", }, GeneratedSharedTermEntry { locale: LocaleId::ZhCN, - key: "agents.code", - value: "代码会话", + key: "agents.ComputerUse", + value: "电脑操作", }, GeneratedSharedTermEntry { locale: LocaleId::ZhCN, - key: "agents.cowork", - value: "协作会话", + key: "agents.Cowork", + value: "办公协作", + }, + GeneratedSharedTermEntry { + locale: LocaleId::ZhCN, + key: "agents.Creative", + value: "创造", + }, + GeneratedSharedTermEntry { + locale: LocaleId::ZhCN, + key: "agents.DeepResearch", + value: "深度研究", }, GeneratedSharedTermEntry { locale: LocaleId::ZhCN, key: "agents.default", value: "默认助手", }, + GeneratedSharedTermEntry { + locale: LocaleId::ZhCN, + key: "agents.Minimal", + value: "极简", + }, + GeneratedSharedTermEntry { + locale: LocaleId::ZhCN, + key: "agents.Standard", + value: "标准", + }, + GeneratedSharedTermEntry { + locale: LocaleId::ZhCN, + key: "agents.Ultimate", + value: "极致", + }, GeneratedSharedTermEntry { locale: LocaleId::ZhCN, key: "connectionMethods.botFeishu", @@ -142,11 +167,6 @@ pub const GENERATED_SHARED_TERMS: &[GeneratedSharedTermEntry] = &[ key: "features.workspace", value: "工作区", }, - GeneratedSharedTermEntry { - locale: LocaleId::ZhCN, - key: "modes.agentic", - value: "代理模式", - }, GeneratedSharedTermEntry { locale: LocaleId::ZhCN, key: "modes.assistant", @@ -234,24 +254,49 @@ pub const GENERATED_SHARED_TERMS: &[GeneratedSharedTermEntry] = &[ }, GeneratedSharedTermEntry { locale: LocaleId::ZhTW, - key: "agents.claw", - value: "Claw", + key: "agents.Claw", + value: "助理", + }, + GeneratedSharedTermEntry { + locale: LocaleId::ZhTW, + key: "agents.ComputerUse", + value: "電腦操作", }, GeneratedSharedTermEntry { locale: LocaleId::ZhTW, - key: "agents.code", - value: "程式碼會話", + key: "agents.Cowork", + value: "辦公協作", }, GeneratedSharedTermEntry { locale: LocaleId::ZhTW, - key: "agents.cowork", - value: "協作會話", + key: "agents.Creative", + value: "創造", + }, + GeneratedSharedTermEntry { + locale: LocaleId::ZhTW, + key: "agents.DeepResearch", + value: "深度研究", }, GeneratedSharedTermEntry { locale: LocaleId::ZhTW, key: "agents.default", value: "預設助手", }, + GeneratedSharedTermEntry { + locale: LocaleId::ZhTW, + key: "agents.Minimal", + value: "極簡", + }, + GeneratedSharedTermEntry { + locale: LocaleId::ZhTW, + key: "agents.Standard", + value: "標準", + }, + GeneratedSharedTermEntry { + locale: LocaleId::ZhTW, + key: "agents.Ultimate", + value: "極致", + }, GeneratedSharedTermEntry { locale: LocaleId::ZhTW, key: "connectionMethods.botFeishu", @@ -307,11 +352,6 @@ pub const GENERATED_SHARED_TERMS: &[GeneratedSharedTermEntry] = &[ key: "features.workspace", value: "工作區", }, - GeneratedSharedTermEntry { - locale: LocaleId::ZhTW, - key: "modes.agentic", - value: "代理模式", - }, GeneratedSharedTermEntry { locale: LocaleId::ZhTW, key: "modes.assistant", @@ -399,24 +439,49 @@ pub const GENERATED_SHARED_TERMS: &[GeneratedSharedTermEntry] = &[ }, GeneratedSharedTermEntry { locale: LocaleId::EnUS, - key: "agents.claw", - value: "Claw", + key: "agents.Claw", + value: "Assistant", }, GeneratedSharedTermEntry { locale: LocaleId::EnUS, - key: "agents.code", - value: "Code Session", + key: "agents.ComputerUse", + value: "Computer Use", }, GeneratedSharedTermEntry { locale: LocaleId::EnUS, - key: "agents.cowork", - value: "Cowork Session", + key: "agents.Cowork", + value: "Cowork", + }, + GeneratedSharedTermEntry { + locale: LocaleId::EnUS, + key: "agents.Creative", + value: "Creative", + }, + GeneratedSharedTermEntry { + locale: LocaleId::EnUS, + key: "agents.DeepResearch", + value: "Deep Research", }, GeneratedSharedTermEntry { locale: LocaleId::EnUS, key: "agents.default", value: "Default Assistant", }, + GeneratedSharedTermEntry { + locale: LocaleId::EnUS, + key: "agents.Minimal", + value: "Minimal", + }, + GeneratedSharedTermEntry { + locale: LocaleId::EnUS, + key: "agents.Standard", + value: "Standard", + }, + GeneratedSharedTermEntry { + locale: LocaleId::EnUS, + key: "agents.Ultimate", + value: "Ultimate", + }, GeneratedSharedTermEntry { locale: LocaleId::EnUS, key: "connectionMethods.botFeishu", @@ -472,11 +537,6 @@ pub const GENERATED_SHARED_TERMS: &[GeneratedSharedTermEntry] = &[ key: "features.workspace", value: "Workspace", }, - GeneratedSharedTermEntry { - locale: LocaleId::EnUS, - key: "modes.agentic", - value: "Agentic Mode", - }, GeneratedSharedTermEntry { locale: LocaleId::EnUS, key: "modes.assistant", diff --git a/src/crates/assembly/core/src/service/remote_connect/bot/command_router.rs b/src/crates/assembly/core/src/service/remote_connect/bot/command_router.rs index 395ea035d2..393fac70f1 100644 --- a/src/crates/assembly/core/src/service/remote_connect/bot/command_router.rs +++ b/src/crates/assembly/core/src/service/remote_connect/bot/command_router.rs @@ -678,7 +678,7 @@ async fn dispatch( BotCommand::SetVerbose(on) => set_verbose(state, on, s).await, BotCommand::SwitchContext => start_switch(state, s).await, BotCommand::NewSession => new_session_for_mode(state, s).await, - BotCommand::NewCodeSession => guarded_new(state, "agentic", s).await, + BotCommand::NewCodeSession => guarded_new(state, "Standard", s).await, BotCommand::NewCoworkSession => guarded_new(state, "Cowork", s).await, BotCommand::NewClawSession => guarded_new(state, "Claw", s).await, BotCommand::ResumeSession => start_resume(state, 0, s).await, @@ -1855,7 +1855,7 @@ fn truncate_text(text: &str, max_chars: usize) -> String { async fn new_session_for_mode(state: &mut BotChatState, s: &'static BotStrings) -> HandleResult { let agent_type = if state.display_mode == BotDisplayMode::Pro { - "agentic" + "Standard" } else { "Claw" }; @@ -1867,12 +1867,12 @@ async fn guarded_new( agent_type: &str, s: &'static BotStrings, ) -> HandleResult { - let needs_pro = matches!(agent_type, "agentic" | "Cowork"); + let needs_pro = matches!(agent_type, "Standard" | "Cowork"); let needs_assistant = matches!(agent_type, "Claw"); if needs_pro && state.display_mode != BotDisplayMode::Pro { let target_cmd = match agent_type { - "agentic" => "/new_code_session", + "Standard" => "/new_code_session", "Cowork" => "/new_cowork_session", _ => "/new_code_session", }; @@ -2644,10 +2644,10 @@ async fn submit_question_answers( // ── Free-form chat handling ─────────────────────────────────────── /// Look up the agent type a session was created with (e.g. "Claw", "Cowork", -/// "agentic"). Returns `None` if the coordinator is unavailable or the +/// "Standard"). Returns `None` if the coordinator is unavailable or the /// session is not currently hot in memory; in that case `send_message` will /// lazily restore the session from disk and `resolve_agent_type` falls back -/// to the safe default ("agentic"), so chat keeps working. +/// to the safe default ("Standard"), so chat keeps working. async fn resolve_session_agent_type(session_id: &str) -> Option { use crate::agentic::coordination::get_global_coordinator; use crate::service_agent_runtime::CoreServiceAgentRuntime; @@ -2728,18 +2728,18 @@ async fn handle_chat( let turn_id = format!("turn_{}", uuid::Uuid::new_v4()); // Pick the agent type from the actual session — NOT a hardcoded - // "agentic" — otherwise every chat message goes through the Code + // "Standard" — otherwise every chat message goes through the Code // (`agentic`) agent regardless of what kind of session was created. // Concretely: the IM pairing bootstrap creates a `Claw` session for // assistant mode, but the old hardcoded value caused all subsequent // messages to be re-routed to the Code agent and the assistant flow // was effectively bypassed. We mirror the agent type the session was - // actually created with, falling back to "agentic" only if the session + // actually created with, falling back to "Standard" only if the session // is missing in memory (e.g. needs lazy restore — `send_message` will // also normalize via `resolve_agent_type`). let agent_type = resolve_session_agent_type(&session_id) .await - .unwrap_or_else(|| "agentic".to_string()); + .unwrap_or_else(|| "Standard".to_string()); // Intentionally do NOT send a "Processing..." / "Queued" interstitial // message with a Cancel-task menu. The session manager queues new user diff --git a/src/crates/assembly/core/src/service_agent_runtime.rs b/src/crates/assembly/core/src/service_agent_runtime.rs index fbfb17d1b7..af48014106 100644 --- a/src/crates/assembly/core/src/service_agent_runtime.rs +++ b/src/crates/assembly/core/src/service_agent_runtime.rs @@ -3435,7 +3435,7 @@ mod tests { let mut session = Session::new_with_id( "session-model-scope".to_string(), "Model scope".to_string(), - "agentic".to_string(), + "Standard".to_string(), Default::default(), ); diff --git a/src/crates/assembly/product-capabilities/src/lib.rs b/src/crates/assembly/product-capabilities/src/lib.rs index 93179474cd..6327753889 100644 --- a/src/crates/assembly/product-capabilities/src/lib.rs +++ b/src/crates/assembly/product-capabilities/src/lib.rs @@ -1023,11 +1023,11 @@ const CREATION_TOOL_GROUPS: &[&str] = &["core.creation", "core.miniapp"]; const CANVAS_TOOL_GROUPS: &[&str] = &["core.canvas"]; const CODE_AGENT_IDS: &[&str] = &[ - "minimal", - "agentic", + "Minimal", + "Standard", "Cowork", "Claw", - "Ultra", + "Ultimate", "SwarmPlanner", "SwarmWorker", "SwarmReviewer", diff --git a/src/crates/assembly/product-capabilities/tests/product_capability_contracts/plugin_product_shape.rs b/src/crates/assembly/product-capabilities/tests/product_capability_contracts/plugin_product_shape.rs index 092e6f9288..be675fa3fd 100644 --- a/src/crates/assembly/product-capabilities/tests/product_capability_contracts/plugin_product_shape.rs +++ b/src/crates/assembly/product-capabilities/tests/product_capability_contracts/plugin_product_shape.rs @@ -76,7 +76,7 @@ impl openbitfun_runtime_ports::AgentSubmissionPort for ProductShapeSubmissionPor } async fn resolve_session_agent_type(&self, _session_id: &str) -> PortResult> { - Ok(Some("agentic".to_string())) + Ok(Some("Standard".to_string())) } } diff --git a/src/crates/assembly/product-capabilities/tests/product_capability_contracts/product_sdk_assembly.rs b/src/crates/assembly/product-capabilities/tests/product_capability_contracts/product_sdk_assembly.rs index 2207414456..d5a171f035 100644 --- a/src/crates/assembly/product-capabilities/tests/product_capability_contracts/product_sdk_assembly.rs +++ b/src/crates/assembly/product-capabilities/tests/product_capability_contracts/product_sdk_assembly.rs @@ -48,7 +48,7 @@ impl AgentSubmissionPort for ProductSdkAgentProvider { } async fn resolve_session_agent_type(&self, _session_id: &str) -> PortResult> { - Ok(Some("agentic".to_string())) + Ok(Some("Standard".to_string())) } } @@ -99,7 +99,7 @@ async fn sdk_delivery_profile_builds_shared_runtime_owner_ceiling_without_openbi let handle = runtime .run(AgentRunRequest::new( - SessionSelector::create("SDK profile smoke", "agentic", None), + SessionSelector::create("SDK profile smoke", "Standard", None), "hello from sdk profile", )) .await @@ -145,7 +145,7 @@ async fn product_runtime_parts_can_build_agent_runtime_sdk_without_core() { AgentRunRequest::new( SessionSelector::create( "Product SDK smoke", - "agentic", + "Standard", Some("/workspace/project".to_string()), ), "hello from product assembly", @@ -158,7 +158,7 @@ async fn product_runtime_parts_can_build_agent_runtime_sdk_without_core() { assert_eq!(handle.session_id, "product-sdk-session"); assert_eq!(handle.turn_id, "product-sdk-turn"); - assert_eq!(handle.agent_type.as_deref(), Some("agentic")); + assert_eq!(handle.agent_type.as_deref(), Some("Standard")); assert!(handle.accepted); assert_eq!( handle.events.expect("event stream").snapshot(), diff --git a/src/crates/assembly/product-capabilities/tests/product_capability_contracts/runtime_boundary.rs b/src/crates/assembly/product-capabilities/tests/product_capability_contracts/runtime_boundary.rs index 901a325c61..18af862ec7 100644 --- a/src/crates/assembly/product-capabilities/tests/product_capability_contracts/runtime_boundary.rs +++ b/src/crates/assembly/product-capabilities/tests/product_capability_contracts/runtime_boundary.rs @@ -108,11 +108,11 @@ fn headless_agent_hosts_keep_explore_code_agents_without_product_workflow_agents } for code_agent in [ - "minimal", - "agentic", + "Minimal", + "Standard", "Explore", "GeneralPurpose", - "Ultra", + "Ultimate", "SwarmPlanner", "SwarmWorker", "SwarmReviewer", diff --git a/src/crates/contracts/config-contracts/src/agent_identity_migration.rs b/src/crates/contracts/config-contracts/src/agent_identity_migration.rs new file mode 100644 index 0000000000..0fbca9838b --- /dev/null +++ b/src/crates/contracts/config-contracts/src/agent_identity_migration.rs @@ -0,0 +1,142 @@ +//! Read compatibility for the retired built-in Agent/profile identities. +//! Migration is atomic: ambiguous collisions return an error before any write. + +use openbitfun_core_types::agent_identity::canonical_agent_config_id; +use serde_json::{Map, Value}; + +pub fn canonicalize_agent_profile_keys( + profiles: &Map, +) -> Result, String> { + let mut result = Map::new(); + for (old_id, value) in profiles { + let id = canonical_agent_config_id(old_id); + let mut value = value.clone(); + if let Some(object) = value.as_object_mut() { + if object.contains_key("profile_id") { + object.insert("profile_id".into(), Value::String(id.into())); + } + } + if let Some(existing) = result.get_mut(id) { + merge_compatible(existing, value).map_err(|()| { + format!("Conflicting Agent profile aliases for '{id}' (including '{old_id}'); original configuration was preserved") + })?; + } else { + result.insert(id.into(), value); + } + } + Ok(result) +} + +fn merge_compatible(existing: &mut Value, incoming: Value) -> Result<(), ()> { + if existing == &incoming || incoming.is_null() { + return Ok(()); + } + if existing.is_null() { + *existing = incoming; + return Ok(()); + } + if let (Value::Object(existing), Value::Object(incoming)) = (existing, incoming) { + for (key, value) in incoming { + if let Some(previous) = existing.get_mut(&key) { + merge_compatible(previous, value)?; + } else { + existing.insert(key, value); + } + } + Ok(()) + } else { + Err(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn migrates_legacy_keys_and_references_without_changing_settings() { + let raw = json!({ + "coding_shared": {"profile_id": "coding_shared", "removed_tools": ["Bash"], "unknown_future_field": true}, + "Standard": {"enabled_user_skills": ["user::docs"]}, + "Ultra": {"profile_id": "Ultra", "subagent_overrides": {"SwarmWorker": "disabled"}}, + "plugin::agentic": {"profile_id": "plugin::agentic"} + }); + let migrated = canonicalize_agent_profile_keys(raw.as_object().unwrap()).unwrap(); + assert_eq!(migrated["Standard"]["profile_id"], "Standard"); + assert_eq!(migrated["Standard"]["removed_tools"], json!(["Bash"])); + assert_eq!( + migrated["Standard"]["enabled_user_skills"], + json!(["user::docs"]) + ); + assert_eq!(migrated["Standard"]["unknown_future_field"], true); + assert_eq!(migrated["Ultimate"]["profile_id"], "Ultimate"); + assert!(migrated.contains_key("plugin::agentic")); + assert!(!migrated.contains_key("coding_shared")); + assert_eq!( + canonicalize_agent_profile_keys(&migrated).unwrap(), + migrated + ); + } + + #[test] + fn conflicting_aliases_do_not_mutate_original_records() { + let raw = + json!({"agentic": {"removed_tools": ["Bash"]}, "Standard": {"removed_tools": []}}); + let before = raw.clone(); + assert!(canonicalize_agent_profile_keys(raw.as_object().unwrap()).is_err()); + assert_eq!(raw, before); + } + + #[test] + fn typed_profiles_round_trip_legacy_keys_and_future_fields() { + let ai: crate::types::AIConfig = serde_json::from_value(json!({ + "agent_profiles": { + "coding_shared": {"removed_tools": ["Bash"], "future_policy": {"enabled": false}}, + "Ultra": {"profile_id": "Ultra", "enabled_user_skills": ["user::docs"]}, + "custom::agentic": {"disabled_user_skills": ["user::private"]} + } + })) + .unwrap(); + assert_eq!(ai.agent_profiles["Standard"].profile_id, "Standard"); + assert_eq!(ai.agent_profiles["Ultimate"].profile_id, "Ultimate"); + let saved = serde_json::to_value(&ai).unwrap(); + assert_eq!( + saved["agent_profiles"]["Standard"]["future_policy"], + json!({"enabled": false}) + ); + assert_eq!( + saved["agent_profiles"]["Standard"]["removed_tools"], + json!(["Bash"]) + ); + assert!(saved["agent_profiles"].get("coding_shared").is_none()); + assert!(saved["agent_profiles"].get("custom::agentic").is_some()); + let reloaded: crate::types::AIConfig = serde_json::from_value(saved.clone()).unwrap(); + assert_eq!(serde_json::to_value(reloaded).unwrap(), saved); + } + + #[test] + fn legacy_selector_preferences_keep_their_selection_strategy() { + use crate::types::{AppFlowChatConfig, ChatInputDefaultModeStrategy}; + let fixed: AppFlowChatConfig = serde_json::from_value(json!({ + "default_mode_id": "agentic", "last_mode_id": "Ultra" + })) + .unwrap(); + assert_eq!(fixed.default_mode_strategy, None); + assert_eq!(fixed.default_mode_id.as_deref(), Some("Standard")); + assert_eq!(fixed.last_mode_id.as_deref(), Some("Ultimate")); + let following: AppFlowChatConfig = serde_json::from_value(json!({ + "default_mode_strategy": "follow_last", "last_mode_id": "minimal", + "show_permission_mode_control": false + })) + .unwrap(); + let written = serde_json::to_value(&following).unwrap(); + assert_eq!(written["last_mode_id"], "Minimal"); + let reloaded: AppFlowChatConfig = serde_json::from_value(written).unwrap(); + assert_eq!( + reloaded.default_mode_strategy, + Some(ChatInputDefaultModeStrategy::FollowLast) + ); + assert!(!reloaded.show_permission_mode_control); + } +} diff --git a/src/crates/contracts/config-contracts/src/lib.rs b/src/crates/contracts/config-contracts/src/lib.rs index 7c1e401c20..dae379f4fc 100644 --- a/src/crates/contracts/config-contracts/src/lib.rs +++ b/src/crates/contracts/config-contracts/src/lib.rs @@ -1,4 +1,5 @@ //! Persisted configuration DTOs and pure compatibility validation. +pub mod agent_identity_migration; pub mod conversion; mod types; pub use types::*; diff --git a/src/crates/contracts/config-contracts/src/types.rs b/src/crates/contracts/config-contracts/src/types.rs index 78e4fc2a6f..6ddbd4985d 100644 --- a/src/crates/contracts/config-contracts/src/types.rs +++ b/src/crates/contracts/config-contracts/src/types.rs @@ -15,12 +15,22 @@ fn deserialize_agent_profiles<'de, D>( where D: serde::Deserializer<'de>, { - let raw = Option::>>::deserialize(deserializer)?; - Ok(raw - .unwrap_or_default() + let raw = Option::>::deserialize(deserializer)?; + let migrated = + crate::agent_identity_migration::canonicalize_agent_profile_keys(&raw.unwrap_or_default()) + .map_err(serde::de::Error::custom)?; + migrated .into_iter() - .filter_map(|(profile_id, config)| config.map(|config| (profile_id, config))) - .collect()) + .filter(|(_, value)| !value.is_null()) + .map(|(id, value)| { + serde_json::from_value(value) + .map(|mut profile: AgentProfileConfig| { + profile.profile_id = id.clone(); + (id, profile) + }) + .map_err(serde::de::Error::custom) + }) + .collect() } /// Web UI font preferences (settings → basics). Keys match `FontPreference` in the frontend (camelCase). @@ -322,10 +332,16 @@ pub struct AppFlowChatConfig { #[serde(skip_serializing_if = "Option::is_none")] pub default_mode_strategy: Option, /// Optional fixed ChatInput mode id. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_optional_agent_id" + )] pub default_mode_id: Option, /// Most recent mode explicitly selected from the ChatInput Harness control. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_optional_agent_id" + )] pub last_mode_id: Option, /// Whether the chat input exposes the global permission-mode shortcut. /// @@ -1307,11 +1323,11 @@ impl AIConfig { /// Shared agent-profile configuration. /// -/// Tool and skill configuration shared by compatible mode profiles. +/// Tool, skill, and delegation overrides owned by one Agent identity. #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default)] pub struct AgentProfileConfig { - /// Shared profile ID (e.g. agentic, coding_shared, requirement, ui-design). + /// Canonical Agent ID (e.g. Standard, Creative, Claw, or a custom Agent ID). pub profile_id: String, /// Tools explicitly enabled by the user that are not part of the mode defaults. @@ -1337,6 +1353,10 @@ pub struct AgentProfileConfig { /// Agent-level permission rules applied after project rules. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tool_permission_rules: Vec, + + /// Preserve fields written by newer versions during migration and updates. + #[serde(flatten)] + pub extensions: serde_json::Map, } /// User-level Skill configuration shared by every agent profile. @@ -2298,7 +2318,7 @@ mod tests { #[test] fn legacy_agent_profile_defaults_permission_rules_and_omits_empty_field() { let config: AgentProfileConfig = serde_json::from_value(serde_json::json!({ - "profile_id": "coding_shared", + "profile_id": "Standard", "added_tools": ["read"] })) .expect("legacy agent profile should deserialize"); @@ -2680,7 +2700,7 @@ mod tests { "app": { "flow_chat": { "default_mode_strategy": "follow_last", - "default_mode_id": "Ultra", + "default_mode_id": "Ultimate", "last_mode_id": "Creative" } } @@ -2692,7 +2712,7 @@ mod tests { ); assert_eq!( configured.app.flow_chat.default_mode_id.as_deref(), - Some("Ultra") + Some("Ultimate") ); assert_eq!( configured.app.flow_chat.last_mode_id.as_deref(), diff --git a/src/crates/contracts/core-types/src/agent_identity.rs b/src/crates/contracts/core-types/src/agent_identity.rs new file mode 100644 index 0000000000..e3047a370e --- /dev/null +++ b/src/crates/contracts/core-types/src/agent_identity.rs @@ -0,0 +1,89 @@ +// Generated by scripts/generate-agent-harness.mjs. Edit the shared contract instead. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum HarnessId { + #[serde(alias = "minimal")] + Minimal, + #[serde(alias = "standard")] + #[serde(alias = "balanced")] + #[serde(alias = "agentic")] + #[serde(alias = "Agentic")] + Standard, + #[serde(alias = "ultimate")] + #[serde(alias = "Ultra")] + #[serde(alias = "ultra")] + Ultimate, + #[serde(alias = "creative")] + Creative, +} + +impl HarnessId { + pub const ALL: [Self; 4] = [ + Self::Minimal, + Self::Standard, + Self::Ultimate, + Self::Creative, + ]; + pub const fn as_str(self) -> &'static str { + match self { + Self::Minimal => "Minimal", + Self::Standard => "Standard", + Self::Ultimate => "Ultimate", + Self::Creative => "Creative", + } + } + pub fn from_legacy_id(id: &str) -> Option { + match id.trim().to_ascii_lowercase().as_str() { + "minimal" => Some(Self::Minimal), + "standard" | "balanced" | "agentic" => Some(Self::Standard), + "ultimate" | "ultra" => Some(Self::Ultimate), + "creative" => Some(Self::Creative), + _ => None, + } + } + pub const fn legacy_wire_id(self) -> &'static str { + match self { + Self::Minimal => "minimal", + Self::Standard => "agentic", + Self::Ultimate => "Ultra", + Self::Creative => "Creative", + } + } + pub const fn legacy_ids(self) -> &'static [&'static str] { + match self { + Self::Minimal => &["minimal"], + Self::Standard => &["standard", "balanced", "agentic", "Agentic"], + Self::Ultimate => &["ultimate", "Ultra", "ultra"], + Self::Creative => &["creative"], + } + } +} + +/// Normalize a built-in identity at storage/protocol boundaries. Unknown IDs survive. +pub fn canonical_agent_id(id: &str) -> &str { + HarnessId::from_legacy_id(id) + .map(HarnessId::as_str) + .unwrap_or(id) +} + +pub fn canonical_agent_config_id(id: &str) -> &str { + match id { + "coding_shared" => "Standard", + _ => canonical_agent_id(id), + } +} + +pub fn deserialize_agent_id<'de, D: serde::Deserializer<'de>>( + deserializer: D, +) -> Result { + let id = String::deserialize(deserializer)?; + Ok(canonical_agent_id(&id).to_owned()) +} + +pub fn deserialize_optional_agent_id<'de, D: serde::Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + Ok(Option::::deserialize(deserializer)?.map(|id| canonical_agent_id(&id).to_owned())) +} diff --git a/src/crates/contracts/core-types/src/agent_identity_wire.rs b/src/crates/contracts/core-types/src/agent_identity_wire.rs new file mode 100644 index 0000000000..09d40ef33e --- /dev/null +++ b/src/crates/contracts/core-types/src/agent_identity_wire.rs @@ -0,0 +1,277 @@ +//! Compatibility adapter for the existing peer/mobile wire dialect. +//! Domain records always use canonical IDs; supported older clients still speak v0. +use crate::agent_identity::{canonical_agent_config_id, canonical_agent_id, HarnessId}; +use serde_json::{Map, Value}; + +#[derive(Clone, Copy)] +pub enum AgentIdentityDialect { + Canonical, + Legacy, +} + +pub fn serialize_legacy_agent_id( + id: &str, + serializer: S, +) -> Result { + serializer.serialize_str(&identity(id, AgentIdentityDialect::Legacy, false)) +} + +fn identity(id: &str, dialect: AgentIdentityDialect, profile: bool) -> String { + let canonical = if profile { + canonical_agent_config_id(id) + } else { + canonical_agent_id(id) + }; + match dialect { + AgentIdentityDialect::Canonical => canonical.to_owned(), + AgentIdentityDialect::Legacy if profile && canonical == "Standard" => { + "coding_shared".into() + } + AgentIdentityDialect::Legacy => HarnessId::from_legacy_id(canonical) + .map(|id| id.legacy_wire_id()) + .unwrap_or(canonical) + .to_owned(), + } +} + +/// Only product identity fields are translated. Prompt text, tool arguments, +/// user metadata, external definitions, and arbitrary config values are opaque. +pub fn translate_agent_identity_fields( + value: &mut Value, + dialect: AgentIdentityDialect, +) -> Result<(), String> { + match value { + Value::Array(items) => { + for item in items { + translate_agent_identity_fields(item, dialect)?; + } + } + Value::Object(object) => { + if object + .get("source") + .and_then(Value::as_str) + .is_some_and(|s| s != "builtin") + { + return Ok(()); + } + let catalog_entry = + object.contains_key("toolCount") || object.contains_key("defaultTools"); + for (key, value) in object { + if matches!(key.as_str(), "agent_profiles" | "ai.agent_profiles") { + translate_profile_map(value, dialect)?; + continue; + } + let profile = matches!(key.as_str(), "configProfileId" | "profile_id"); + let agent = matches!( + key.as_str(), + "agentType" + | "agent_type" + | "parentAgentType" + | "parent_agent_type" + | "modeId" + | "mode_id" + | "default_mode_id" + | "last_mode_id" + | "lastUserDialogAgentType" + | "last_user_dialog_agent_type" + | "lastSubmittedAgentType" + | "last_submitted_agent_type" + ) || (catalog_entry && key == "id"); + if profile || agent { + if let Some(id) = value.as_str() { + *value = Value::String(identity(id, dialect, profile)); + } + } else if matches!( + key.as_str(), + "configProfileMemberModeIds" | "allowedParentAgentIds" | "deniedParentAgentIds" + ) { + if let Some(ids) = value.as_array_mut() { + for id in ids { + if let Some(text) = id.as_str() { + *id = Value::String(identity(text, dialect, false)); + } + } + } + } else if !matches!( + key.as_str(), + "content" + | "prompt" + | "arguments" + | "args" + | "input" + | "output" + | "metadata" + | "userMessageMetadata" + | "user_message_metadata" + | "value" + ) { + translate_agent_identity_fields(value, dialect)?; + } + } + } + _ => {} + } + Ok(()) +} + +fn merge_profile_records(previous: &mut Value, incoming: Value) -> Result<(), String> { + if previous == &incoming || incoming.is_null() { + return Ok(()); + } + if previous.is_null() { + *previous = incoming; + return Ok(()); + } + if let (Value::Object(previous), Value::Object(incoming)) = (previous, incoming) { + for (key, value) in incoming { + if let Some(existing) = previous.get_mut(&key) { + merge_profile_records(existing, value)?; + } else { + previous.insert(key, value); + } + } + return Ok(()); + } + Err("Conflicting Agent profile aliases; original configuration was preserved".into()) +} + +fn translate_profile(value: &mut Value, dialect: AgentIdentityDialect) { + if let Some(id) = value.get_mut("profile_id") { + if let Some(text) = id.as_str() { + *id = Value::String(identity(text, dialect, true)); + } + } +} + +fn translate_profile_map(value: &mut Value, dialect: AgentIdentityDialect) -> Result<(), String> { + if let Some(profiles) = value.as_object() { + let mut translated = Map::new(); + for (id, config) in profiles { + let key = identity(id, dialect, true); + let mut config = config.clone(); + translate_profile(&mut config, dialect); + if let Some(existing) = translated.get_mut(&key) { + merge_profile_records(existing, config)?; + } else { + translated.insert(key, config); + } + } + *value = Value::Object(translated); + } + Ok(()) +} + +fn config_path(path: &str, dialect: AgentIdentityDialect) -> String { + let Some(rest) = path.strip_prefix("ai.agent_profiles.") else { + return path.into(); + }; + let (id, suffix) = rest.split_once('.').unwrap_or((rest, "")); + let suffix = if suffix.is_empty() { + String::new() + } else { + format!(".{suffix}") + }; + format!("ai.agent_profiles.{}{suffix}", identity(id, dialect, true)) +} + +fn config_value( + path: &str, + value: &mut Value, + dialect: AgentIdentityDialect, +) -> Result<(), String> { + if path == "ai.agent_profiles" { + return translate_profile_map(value, dialect); + } + if matches!( + path, + "app.flow_chat.default_mode_id" | "app.flow_chat.last_mode_id" + ) { + if let Some(id) = value.as_str() { + *value = Value::String(identity(id, dialect, false)); + } + } else if let Some(rest) = path.strip_prefix("ai.agent_profiles.") { + match rest.split_once('.') { + None => translate_profile(value, dialect), + Some((_, "profile_id")) => { + if let Some(id) = value.as_str() { + *value = Value::String(identity(id, dialect, true)); + } + } + _ => {} + } + } else if matches!(path, "" | "app" | "app.flow_chat" | "ai") { + translate_agent_identity_fields(value, dialect)?; + } + Ok(()) +} + +pub fn translate_agent_identity_command( + command: &str, + mut value: Value, + dialect: AgentIdentityDialect, +) -> Result { + translate_agent_identity_fields(&mut value, dialect)?; + if matches!(command, "set_config" | "get_config" | "get_configs") { + let request = if value.get("request").is_some() { + value.get_mut("request").unwrap() + } else { + &mut value + }; + if let Some(request) = request.as_object_mut() { + let path = request + .get("path") + .and_then(Value::as_str) + .unwrap_or_default() + .to_owned(); + if command == "set_config" { + if let Some(config) = request.get_mut("value") { + config_value(&path, config, dialect)?; + } + } + if request.contains_key("path") { + request.insert("path".into(), Value::String(config_path(&path, dialect))); + } + if command == "get_configs" { + if let Some(paths) = request.get_mut("paths").and_then(Value::as_array_mut) { + for path in paths { + if let Some(text) = path.as_str() { + *path = Value::String(config_path(text, dialect)); + } + } + } + } + } + } + Ok(value) +} + +/// A scoped config reply may be a scalar, profile record, or keyed profile map. +pub fn translate_agent_identity_response( + command: &str, + mut value: Value, + args: &Value, + dialect: AgentIdentityDialect, +) -> Result { + if command == "get_agent_profile_configs" { + translate_profile_map(&mut value, dialect)?; + } else if command == "get_config" { + let request = args.get("request").unwrap_or(args); + let path = request + .get("path") + .and_then(Value::as_str) + .unwrap_or_default(); + config_value(path, &mut value, dialect)?; + } else if command == "get_configs" { + if let Some(configs) = value.as_object_mut() { + let mut translated = Map::new(); + for (path, mut config) in std::mem::take(configs) { + config_value(&path, &mut config, dialect)?; + translated.insert(config_path(&path, dialect), config); + } + *configs = translated; + } + } else { + translate_agent_identity_fields(&mut value, dialect)?; + } + Ok(value) +} diff --git a/src/crates/contracts/core-types/src/lib.rs b/src/crates/contracts/core-types/src/lib.rs index ff08aad5e3..beab66bea1 100644 --- a/src/crates/contracts/core-types/src/lib.rs +++ b/src/crates/contracts/core-types/src/lib.rs @@ -3,6 +3,8 @@ //! This crate must stay lightweight: do not add runtime, network, platform, or //! product assembly dependencies here. +pub mod agent_identity; +pub mod agent_identity_wire; pub mod ai; pub mod errors; pub mod installer_config_handoff; diff --git a/src/crates/contracts/core-types/tests/core_type_contracts.rs b/src/crates/contracts/core-types/tests/core_type_contracts.rs index 3b5266bd91..d3ad809a74 100644 --- a/src/crates/contracts/core-types/tests/core_type_contracts.rs +++ b/src/crates/contracts/core-types/tests/core_type_contracts.rs @@ -1,3 +1,5 @@ +#[path = "core_type_contracts/agent_identity.rs"] +mod agent_identity; #[path = "core_type_contracts/session_contracts.rs"] mod session_contracts; #[path = "core_type_contracts/session_usage_contracts.rs"] diff --git a/src/crates/contracts/core-types/tests/core_type_contracts/agent_identity.rs b/src/crates/contracts/core-types/tests/core_type_contracts/agent_identity.rs new file mode 100644 index 0000000000..26e03bb920 --- /dev/null +++ b/src/crates/contracts/core-types/tests/core_type_contracts/agent_identity.rs @@ -0,0 +1,138 @@ +use openbitfun_core_types::agent_identity::{ + canonical_agent_config_id, canonical_agent_id, HarnessId, +}; + +#[test] +fn legacy_harness_ids_read_as_canonical_and_write_only_canonical_ids() { + for (legacy, canonical) in [ + ("minimal", "Minimal"), + ("balanced", "Standard"), + ("agentic", "Standard"), + ("Agentic", "Standard"), + ("Ultra", "Ultimate"), + ("ultimate", "Ultimate"), + ("creative", "Creative"), + ] { + let parsed: HarnessId = serde_json::from_value(serde_json::json!(legacy)).unwrap(); + assert_eq!(parsed.as_str(), canonical); + assert_eq!(serde_json::to_value(parsed).unwrap(), canonical); + assert_eq!( + HarnessId::from_legacy_id(parsed.legacy_wire_id()), + Some(parsed) + ); + } +} + +#[test] +fn unknown_and_source_qualified_agent_identity_survives_normalization() { + for id in [ + "Claw", + "DeepResearch", + "custom::agentic", + "external::Ultra", + "FutureHarness", + ] { + assert_eq!(canonical_agent_id(id), id); + } + assert_eq!(canonical_agent_config_id("coding_shared"), "Standard"); + assert_eq!(canonical_agent_id("coding_shared"), "coding_shared"); +} + +#[test] +fn peer_wire_round_trip_changes_only_product_identity_fields() { + use openbitfun_core_types::agent_identity_wire::{ + translate_agent_identity_command, AgentIdentityDialect, + }; + let current = serde_json::json!({"request": { + "agentType": "Standard", "modeId": "Ultimate", + "config": {"profile_id": "Standard"}, + "userMessageMetadata": {"agentType": "Standard"}, + "arguments": {"agentType": "Standard"}, + "prompt": "agentic Ultra Minimal" + }}); + let wire = translate_agent_identity_command( + "start_dialog_turn", + current.clone(), + AgentIdentityDialect::Legacy, + ) + .unwrap(); + assert_eq!(wire["request"]["agentType"], "agentic"); + assert_eq!(wire["request"]["modeId"], "Ultra"); + assert_eq!(wire["request"]["config"]["profile_id"], "coding_shared"); + assert_eq!(wire["request"]["arguments"]["agentType"], "Standard"); + assert_eq!( + wire["request"]["userMessageMetadata"]["agentType"], + "Standard" + ); + assert_eq!( + translate_agent_identity_command( + "start_dialog_turn", + wire, + AgentIdentityDialect::Canonical + ) + .unwrap(), + current + ); + let external = serde_json::json!({"source":"external", "id":"agentic", "toolCount":2}); + assert_eq!( + translate_agent_identity_command( + "get_available_modes", + external.clone(), + AgentIdentityDialect::Canonical + ) + .unwrap(), + external + ); +} + +#[test] +fn scoped_config_wire_contract_matches_all_surfaces() { + use openbitfun_core_types::agent_identity_wire::{ + translate_agent_identity_command, translate_agent_identity_response, AgentIdentityDialect, + }; + let fixtures: Vec = serde_json::from_str(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../../shared/agent-harness/wire-fixtures.json" + ))) + .unwrap(); + for fixture in fixtures { + let command = fixture["command"].as_str().unwrap(); + for (from, to, dialect) in [ + ("canonical", "legacy", AgentIdentityDialect::Legacy), + ("legacy", "canonical", AgentIdentityDialect::Canonical), + ] { + assert_eq!( + translate_agent_identity_command(command, fixture[from].clone(), dialect).unwrap(), + fixture[to] + ); + if let Some(response) = fixture.get(format!("{from}Response")) { + assert_eq!( + translate_agent_identity_response( + command, + response.clone(), + &fixture[from], + dialect + ) + .unwrap(), + fixture[format!("{to}Response")] + ); + } + } + } +} + +#[test] +fn conflicting_profile_aliases_fail_before_dispatch() { + use openbitfun_core_types::agent_identity_wire::{ + translate_agent_identity_command, AgentIdentityDialect, + }; + let request = serde_json::json!({"request": {"path":"ai.agent_profiles", "value": { + "agentic": {"removed_tools": ["Bash"]}, "Standard": {"removed_tools": ["Read"]} + }}}); + assert!(translate_agent_identity_command( + "set_config", + request, + AgentIdentityDialect::Canonical + ) + .is_err()); +} diff --git a/src/crates/contracts/events/src/frontend_projection.rs b/src/crates/contracts/events/src/frontend_projection.rs index a339ba4f40..98fe686a67 100644 --- a/src/crates/contracts/events/src/frontend_projection.rs +++ b/src/crates/contracts/events/src/frontend_projection.rs @@ -535,7 +535,7 @@ mod tests { let projected = project_agentic_frontend_event(AgenticEvent::SessionCreated { session_id: "session-1".to_string(), session_name: "Isolated task".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some("/worktrees/wt-1".to_string()), project_workspace_path: Some("/repo".to_string()), execution_target: Some(SessionExecutionTarget { diff --git a/src/crates/contracts/product-domains/tests/product_domain_contracts/tool_permission_contracts.rs b/src/crates/contracts/product-domains/tests/product_domain_contracts/tool_permission_contracts.rs index b91a5b8957..9e2906e6af 100644 --- a/src/crates/contracts/product-domains/tests/product_domain_contracts/tool_permission_contracts.rs +++ b/src/crates/contracts/product-domains/tests/product_domain_contracts/tool_permission_contracts.rs @@ -459,7 +459,7 @@ fn permission_request_correlation_fields_use_stable_wire_shape() { project_path: Some("/workspace/project".to_string()), project_id: "project-1".to_string(), session_id: "session-1".to_string(), - agent_id: "agentic".to_string(), + agent_id: "Standard".to_string(), action: "read".to_string(), resources: vec!["README.md".to_string()], save_resources: Vec::new(), diff --git a/src/crates/contracts/runtime-ports/src/agent_api.rs b/src/crates/contracts/runtime-ports/src/agent_api.rs index 7a2b87563b..aa1a9b52c6 100644 --- a/src/crates/contracts/runtime-ports/src/agent_api.rs +++ b/src/crates/contracts/runtime-ports/src/agent_api.rs @@ -2407,7 +2407,7 @@ mod tests { Ok(AgentSessionForkResult { session_id: format!("{}-fork", request.source_session_id), session_name: "Fork".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), }) } } @@ -2491,7 +2491,7 @@ mod tests { fn agent_session_create_request_keeps_rust_literal_compatible() { let request = AgentSessionCreateRequest { session_name: "Generated session".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some("/workspace/project".to_string()), project_workspace_path: None, execution_target: None, @@ -2513,7 +2513,7 @@ mod tests { fn agent_session_create_request_keeps_legacy_payload_compatible() { let request: AgentSessionCreateRequest = serde_json::from_value(serde_json::json!({ "sessionName": "Generated session", - "agentType": "agentic", + "agentType": "Standard", "workspacePath": "/workspace/project" })) .expect("deserialize legacy create request"); @@ -2529,7 +2529,7 @@ mod tests { let legacy = serde_json::json!({ "sessionId": "session_1", "sessionName": "Main", - "agentType": "agentic" + "agentType": "Standard" }); let result: AgentSessionCreateResult = @@ -2551,7 +2551,7 @@ mod tests { let result: AgentSessionCreateResult = serde_json::from_value(serde_json::json!({ "sessionId": "session_1", "sessionName": "Main", - "agentType": "agentic", + "agentType": "Standard", "modelId": "provider/model", "workspacePath": "/worktrees/session_1", "workspaceId": "workspace_1", @@ -3025,7 +3025,7 @@ mod tests { original_message: Some("raw hello".to_string()), turn_id: Some("turn_1".to_string()), execution: Default::default(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some("/workspace/project".to_string()), remote_connection_id: Some("conn-1".to_string()), remote_ssh_host: Some("host-1".to_string()), @@ -3058,7 +3058,7 @@ mod tests { assert_eq!(json["outputSchema"]["type"], "object"); assert_eq!(json["originalMessage"], "raw hello"); assert_eq!(json["turnId"], "turn_1"); - assert_eq!(json["agentType"], "agentic"); + assert_eq!(json["agentType"], "Standard"); assert_eq!(json["workspacePath"], "/workspace/project"); assert_eq!(json["remoteConnectionId"], "conn-1"); assert_eq!(json["remoteSshHost"], "host-1"); @@ -3136,7 +3136,7 @@ mod tests { ); let request = AgentBackgroundResultRequest { session_id: "session_1".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some("/workspace/project".to_string()), remote_connection_id: Some("conn-1".to_string()), remote_ssh_host: Some("host-1".to_string()), @@ -3148,7 +3148,7 @@ mod tests { let json = serde_json::to_value(request).expect("serialize background result request"); assert_eq!(json["sessionId"], "session_1"); - assert_eq!(json["agentType"], "agentic"); + assert_eq!(json["agentType"], "Standard"); assert_eq!(json["workspacePath"], "/workspace/project"); assert_eq!(json["remoteConnectionId"], "conn-1"); assert_eq!(json["remoteSshHost"], "host-1"); @@ -3161,7 +3161,7 @@ mod tests { fn agent_thread_goal_delivery_request_serializes_lifecycle_contract() { let request = AgentThreadGoalDeliveryRequest { session_id: "session_1".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some("/workspace/project".to_string()), remote_connection_id: Some("conn-1".to_string()), remote_ssh_host: Some("host-1".to_string()), @@ -3183,7 +3183,7 @@ mod tests { let json = serde_json::to_value(request).expect("serialize thread goal delivery request"); assert_eq!(json["sessionId"], "session_1"); - assert_eq!(json["agentType"], "agentic"); + assert_eq!(json["agentType"], "Standard"); assert_eq!(json["workspacePath"], "/workspace/project"); assert_eq!(json["remoteConnectionId"], "conn-1"); assert_eq!(json["remoteSshHost"], "host-1"); @@ -3404,11 +3404,11 @@ mod tests { let summary = AgentSessionSummary { session_id: "session_1".to_string(), session_name: "Main".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), model_id: Some("provider/model".to_string()), reasoning_preset: Some("high".to_string()), last_user_dialog_agent_type: Some("plan".to_string()), - last_submitted_agent_type: Some("agentic".to_string()), + last_submitted_agent_type: Some("Standard".to_string()), turn_count: 3, created_at_ms: 1000, last_active_at_ms: 2000, @@ -3465,7 +3465,7 @@ mod tests { }; let mode_request = AgentSessionModeUpdateRequest { session_id: "session_1".to_string(), - mode_id: "agentic".to_string(), + mode_id: "Standard".to_string(), }; let workspace_request = AgentSessionWorkspaceRequest { session_id: "session_1".to_string(), @@ -3503,7 +3503,7 @@ mod tests { assert_eq!(list_json["remoteConnectionId"], "conn-1"); assert_eq!(summary_json["modelId"], "provider/model"); assert_eq!(summary_json["lastUserDialogAgentType"], "plan"); - assert_eq!(summary_json["lastSubmittedAgentType"], "agentic"); + assert_eq!(summary_json["lastSubmittedAgentType"], "Standard"); assert_eq!(list_json["remoteSshHost"], "host-1"); assert_eq!(summary_json["sessionId"], "session_1"); assert_eq!(summary_json["turnCount"], 3); @@ -3524,7 +3524,7 @@ mod tests { assert_eq!(model_json["sessionId"], "session_1"); assert_eq!(model_json["modelId"], "provider/model"); assert_eq!(mode_json["sessionId"], "session_1"); - assert_eq!(mode_json["modeId"], "agentic"); + assert_eq!(mode_json["modeId"], "Standard"); assert_eq!(workspace_json["sessionId"], "session_1"); assert_eq!(binding_json["workspaceId"], "workspace_1"); assert_eq!(binding_json["workspacePath"], "/workspace/project"); diff --git a/src/crates/contracts/runtime-ports/src/lib.rs b/src/crates/contracts/runtime-ports/src/lib.rs index 58e23e2605..d73077be9d 100644 --- a/src/crates/contracts/runtime-ports/src/lib.rs +++ b/src/crates/contracts/runtime-ports/src/lib.rs @@ -325,7 +325,7 @@ impl DelegationPolicy { } pub fn spawn_child_for(self, agent_type: &str) -> Self { - let is_swarm_planner = matches!(agent_type, "Ultra" | "SwarmPlanner"); + let is_swarm_planner = matches!(agent_type, "Ultimate" | "SwarmPlanner"); Self { allow_subagent_spawn: self.scope == DelegationScope::Swarm && is_swarm_planner, nesting_depth: self.nesting_depth.saturating_add(1), diff --git a/src/crates/execution/agent-runtime/examples/sdk_minimal.rs b/src/crates/execution/agent-runtime/examples/sdk_minimal.rs index b6f328e3ce..ad244a2c4f 100644 --- a/src/crates/execution/agent-runtime/examples/sdk_minimal.rs +++ b/src/crates/execution/agent-runtime/examples/sdk_minimal.rs @@ -42,7 +42,7 @@ impl AgentSubmissionPort for ExampleAgentProvider { } async fn resolve_session_agent_type(&self, _session_id: &str) -> PortResult> { - Ok(Some("agentic".to_string())) + Ok(Some("Standard".to_string())) } } @@ -61,7 +61,7 @@ async fn main() -> Result<(), Box> { let handle = runtime .run( AgentRunRequest::new( - SessionSelector::create("Example SDK Session", "agentic", None), + SessionSelector::create("Example SDK Session", "Standard", None), "hello from an SDK embedder", ) .with_source(AgentSubmissionSource::Cli), diff --git a/src/crates/execution/agent-runtime/src/agents.rs b/src/crates/execution/agent-runtime/src/agents.rs index 84a9df9a46..0b5ae34071 100644 --- a/src/crates/execution/agent-runtime/src/agents.rs +++ b/src/crates/execution/agent-runtime/src/agents.rs @@ -6,11 +6,11 @@ use std::borrow::Cow; use std::collections::HashSet; use std::path::Path; -pub const SHARED_CODING_MODE_PROMPT_TEMPLATE: &str = "agentic_mode"; -pub const SHARED_CODING_MODE_CONFIG_PROFILE_ID: &str = "coding_shared"; -pub const SHARED_CODING_MODE_CONFIG_PROFILE_LABEL: &str = "Coding Shared"; -pub const SHARED_CODING_MODE_IDS: &[&str] = &["agentic"]; -pub const SWARM_PLANNER_AGENT_TYPES: &[&str] = &["Ultra", "SwarmPlanner"]; +pub const STANDARD_HARNESS_PROMPT_TEMPLATE: &str = "agentic_mode"; +pub const STANDARD_HARNESS_CONFIG_ID: &str = "Standard"; +pub const STANDARD_HARNESS_CONFIG_LABEL: &str = "Standard"; +pub const STANDARD_HARNESS_CONFIG_MEMBERS: &[&str] = &["Standard"]; +pub const SWARM_PLANNER_AGENT_TYPES: &[&str] = &["Ultimate", "SwarmPlanner"]; pub const SWARM_DELEGATE_AGENT_TYPES: &[&str] = &["SwarmPlanner", "SwarmWorker", "SwarmReviewer"]; pub fn is_swarm_planner_agent_type(agent_type: &str) -> bool { @@ -22,38 +22,35 @@ pub fn is_swarm_delegate_agent_type(agent_type: &str) -> bool { } pub fn resolve_mode_config_profile_id<'a>(mode_id: &'a str) -> Cow<'a, str> { - match mode_id.trim() { - "agentic" => Cow::Borrowed(SHARED_CODING_MODE_CONFIG_PROFILE_ID), - _ => Cow::Borrowed(mode_id), - } + Cow::Borrowed(openbitfun_core_types::agent_identity::canonical_agent_config_id(mode_id)) } pub fn mode_config_profile_member_mode_ids(profile_id: &str) -> &'static [&'static str] { match profile_id.trim() { - SHARED_CODING_MODE_CONFIG_PROFILE_ID => SHARED_CODING_MODE_IDS, + STANDARD_HARNESS_CONFIG_ID => STANDARD_HARNESS_CONFIG_MEMBERS, _ => &[], } } pub fn mode_config_profile_label(profile_id: &str) -> Option<&'static str> { match profile_id.trim() { - SHARED_CODING_MODE_CONFIG_PROFILE_ID => Some(SHARED_CODING_MODE_CONFIG_PROFILE_LABEL), + STANDARD_HARNESS_CONFIG_ID => Some(STANDARD_HARNESS_CONFIG_LABEL), _ => None, } } pub fn mode_presentation_rank(mode_id: &str) -> u8 { match mode_id { - "agentic" => 0, + "Standard" => 0, "Cowork" => 1, "DeepResearch" => 2, - "Ultra" => 3, + "Ultimate" => 3, "Creative" => 4, _ => 99, } } -pub fn shared_coding_mode_user_context_policy() -> UserContextPolicy { +pub fn standard_harness_user_context_policy() -> UserContextPolicy { UserContextPolicy::empty() .with_workspace_context() .with_workspace_instructions() @@ -81,13 +78,13 @@ pub fn builtin_agent_definition_specs() -> Vec { vec![ builtin_agent_spec( - "minimal", + "Minimal", Mode, "primary", SubagentVisibilityPolicy::default(), ), builtin_agent_spec( - "agentic", + "Standard", Mode, "primary", SubagentVisibilityPolicy::default(), @@ -112,7 +109,7 @@ pub fn builtin_agent_definition_specs() -> Vec { SubagentVisibilityPolicy::default(), ), builtin_agent_spec( - "Ultra", + "Ultimate", Mode, "primary", SubagentVisibilityPolicy::default(), @@ -121,19 +118,19 @@ pub fn builtin_agent_definition_specs() -> Vec { "SwarmPlanner", SubAgent, "primary", - SubagentVisibilityPolicy::hidden(["Ultra", "SwarmPlanner"]), + SubagentVisibilityPolicy::hidden(["Ultimate", "SwarmPlanner"]), ), builtin_agent_spec( "SwarmWorker", SubAgent, "primary", - SubagentVisibilityPolicy::hidden(["Ultra", "SwarmPlanner"]), + SubagentVisibilityPolicy::hidden(["Ultimate", "SwarmPlanner"]), ), builtin_agent_spec( "SwarmReviewer", SubAgent, "fast", - SubagentVisibilityPolicy::hidden(["Ultra", "SwarmPlanner"]), + SubagentVisibilityPolicy::hidden(["Ultimate", "SwarmPlanner"]), ), builtin_agent_spec( "ComputerUse", @@ -181,7 +178,7 @@ pub fn builtin_agent_definition_specs() -> Vec { "CodeReview", SubAgent, "primary", - SubagentVisibilityPolicy::hidden(["agentic", "Cowork"]), + SubagentVisibilityPolicy::hidden(["Standard", "Cowork"]), ), builtin_agent_spec( "DeepReview", @@ -206,8 +203,8 @@ pub fn builtin_agent_definition_specs() -> Vec { pub fn default_model_id_for_builtin_agent(agent_type: &str) -> &'static str { match agent_type { - "minimal" | "agentic" | "Cowork" | "Creative" | "ComputerUse" | "Claw" | "DeepResearch" - | "Ultra" => "primary", + "Minimal" | "Standard" | "Cowork" | "Creative" | "ComputerUse" | "Claw" + | "DeepResearch" | "Ultimate" => "primary", "Explore" | "CodeReview" | "GeneralPurpose" | "MemoryPhase2" | "SwarmPlanner" | "SwarmWorker" => "primary", "GenerateDoc" diff --git a/src/crates/execution/agent-runtime/src/context_profile.rs b/src/crates/execution/agent-runtime/src/context_profile.rs index a9df969cd9..60be16c3b1 100644 --- a/src/crates/execution/agent-runtime/src/context_profile.rs +++ b/src/crates/execution/agent-runtime/src/context_profile.rs @@ -207,11 +207,11 @@ impl ContextProfilePolicy { fn is_long_task_agent(agent_type: &str) -> bool { matches!( agent_type, - "agentic" + "Standard" | "DeepReview" | "DeepResearch" | "ComputerUse" - | "Ultra" + | "Ultimate" | "SwarmPlanner" | "SwarmWorker" | "SwarmReviewer" diff --git a/src/crates/execution/agent-runtime/src/permission.rs b/src/crates/execution/agent-runtime/src/permission.rs index ccf7fbfce3..2175389070 100644 --- a/src/crates/execution/agent-runtime/src/permission.rs +++ b/src/crates/execution/agent-runtime/src/permission.rs @@ -837,7 +837,7 @@ mod tests { project_path: None, project_id: "project-1".to_string(), session_id: "session-1".to_string(), - agent_id: "agentic".to_string(), + agent_id: "Standard".to_string(), action: "edit".to_string(), resources: vec!["src/main.rs".to_string()], save_resources: vec!["src/main.rs".to_string()], diff --git a/src/crates/execution/agent-runtime/src/runtime.rs b/src/crates/execution/agent-runtime/src/runtime.rs index 1fe215a37c..601b2cc068 100644 --- a/src/crates/execution/agent-runtime/src/runtime.rs +++ b/src/crates/execution/agent-runtime/src/runtime.rs @@ -2060,7 +2060,7 @@ mod tests { Ok(vec![AgentSessionSummary { session_id: "session_1".to_string(), session_name: "Main".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), model_id: None, reasoning_preset: None, last_user_dialog_agent_type: None, @@ -2139,7 +2139,7 @@ mod tests { Ok(AgentSessionForkResult { session_id: format!("{}-fork", request.source_session_id), session_name: "Forked session".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), }) } @@ -2151,7 +2151,7 @@ mod tests { Ok(AgentSessionForkResult { session_id: "session-fork".to_string(), session_name: "Forked session".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), }) } } @@ -2237,11 +2237,11 @@ mod tests { session: AgentSessionSummary { session_id: "session_1".to_string(), session_name: "Main".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), model_id: Some("provider/model".to_string()), reasoning_preset: Some("high".to_string()), last_user_dialog_agent_type: Some("plan".to_string()), - last_submitted_agent_type: Some("agentic".to_string()), + last_submitted_agent_type: Some("Standard".to_string()), turn_count: 3, created_at_ms: 1000, last_active_at_ms: 2000, @@ -2298,7 +2298,7 @@ mod tests { AgentSessionLineageEntry { session_id: "root_1".to_string(), session_name: "Root".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), created_at_ms: 1, status: AgentSessionLifecycleStatus::Active, active_turn_id: None, @@ -2729,7 +2729,7 @@ mod tests { "fixed-session-id".to_string(), AgentSessionCreateRequest { session_name: "Fixed session".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), agent_route_key: None, workspace_path: Some("/workspace/project".to_string()), project_workspace_path: None, @@ -2761,7 +2761,7 @@ mod tests { "fixed-session-id".to_string(), AgentSessionCreateRequest { session_name: "Fixed session".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), agent_route_key: None, workspace_path: Some("/workspace/project".to_string()), project_workspace_path: None, @@ -2801,7 +2801,7 @@ mod tests { AgentRunRequest::new( SessionSelector::create( "SDK Session", - "agentic", + "Standard", Some("/workspace/project".to_string()), ) .with_metadata(metadata.clone()), @@ -2815,7 +2815,7 @@ mod tests { assert_eq!(handle.session_id, "session_1"); assert_eq!(handle.turn_id, "turn_1"); - assert_eq!(handle.agent_type.as_deref(), Some("agentic")); + assert_eq!(handle.agent_type.as_deref(), Some("Standard")); assert!(handle.accepted); assert_eq!(ports.created_sessions.lock().unwrap()[0].metadata, metadata); assert_eq!( @@ -3420,11 +3420,11 @@ mod tests { session: AgentSessionSummary { session_id: "session_1".to_string(), session_name: "Main".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), model_id: Some("provider/model".to_string()), reasoning_preset: Some("high".to_string()), last_user_dialog_agent_type: Some("plan".to_string()), - last_submitted_agent_type: Some("agentic".to_string()), + last_submitted_agent_type: Some("Standard".to_string()), turn_count: 3, created_at_ms: 1000, last_active_at_ms: 2000, @@ -3525,7 +3525,7 @@ mod tests { original_message: None, turn_id: Some("turn_1".to_string()), execution: Default::default(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some("/workspace/project".to_string()), remote_connection_id: None, remote_ssh_host: None, @@ -3581,7 +3581,7 @@ mod tests { original_message: Some("hello".to_string()), turn_id: Some("turn_1".to_string()), execution: Default::default(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some("/workspace/project".to_string()), remote_connection_id: None, remote_ssh_host: None, @@ -3701,7 +3701,7 @@ mod tests { original_message: None, turn_id: Some("turn-1".to_string()), execution: Default::default(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some("/workspace/project".to_string()), remote_connection_id: None, remote_ssh_host: None, @@ -3877,7 +3877,7 @@ mod tests { let err = runtime .deliver_background_result(AgentBackgroundResultRequest { session_id: "session_1".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: None, remote_connection_id: None, remote_ssh_host: None, @@ -3929,7 +3929,7 @@ mod tests { runtime .deliver_background_result(AgentBackgroundResultRequest { session_id: "session_1".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some("/workspace/project".to_string()), remote_connection_id: Some("conn-1".to_string()), remote_ssh_host: Some("host-1".to_string()), @@ -3943,7 +3943,7 @@ mod tests { runtime .deliver_thread_goal(AgentThreadGoalDeliveryRequest { session_id: "session_1".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: Some("/workspace/project".to_string()), remote_connection_id: Some("conn-1".to_string()), remote_ssh_host: Some("host-1".to_string()), diff --git a/src/crates/execution/agent-runtime/src/scheduler.rs b/src/crates/execution/agent-runtime/src/scheduler.rs index 84876e2323..180bd5b6f8 100644 --- a/src/crates/execution/agent-runtime/src/scheduler.rs +++ b/src/crates/execution/agent-runtime/src/scheduler.rs @@ -1029,7 +1029,7 @@ mod tests { None, None, None, - "agentic".to_string(), + "Standard".to_string(), "input".to_string(), None, DialogSubmissionPolicy::for_source(DialogTriggerSource::Cli), diff --git a/src/crates/execution/agent-runtime/src/session.rs b/src/crates/execution/agent-runtime/src/session.rs index 7348a7044a..77d29ca00b 100644 --- a/src/crates/execution/agent-runtime/src/session.rs +++ b/src/crates/execution/agent-runtime/src/session.rs @@ -21,6 +21,7 @@ pub struct Session { /// This is the mode the next dialog turn should run with by default. It is /// not required to match either the last surviving history turn or the last /// message submission accepted by the scheduler. + #[serde(deserialize_with = "openbitfun_core_types::agent_identity::deserialize_agent_id")] pub agent_type: String, /// Cached mode of the last surviving user dialog turn in history. /// @@ -28,6 +29,9 @@ pub struct Session { /// first-entry vs ongoing mode prompts follow the surviving transcript /// after rollbacks or turn truncation. #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde( + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_optional_agent_id" + )] pub last_user_dialog_agent_type: Option, /// Mode of the most recent user submission accepted by the scheduler. /// @@ -35,6 +39,9 @@ pub struct Session { /// history rollback. It tracks session-level prompt-cache compatibility for /// the next accepted submission. #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde( + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_optional_agent_id" + )] pub last_submitted_agent_type: Option, #[serde( default, @@ -90,6 +97,8 @@ impl CompressionState { impl Session { pub fn new(session_name: String, agent_type: String, config: SessionConfig) -> Self { + let agent_type = + openbitfun_core_types::agent_identity::canonical_agent_id(&agent_type).to_owned(); let now = SystemTime::now(); Self { session_id: Uuid::new_v4().to_string(), @@ -116,6 +125,8 @@ impl Session { agent_type: String, config: SessionConfig, ) -> Self { + let agent_type = + openbitfun_core_types::agent_identity::canonical_agent_id(&agent_type).to_owned(); let now = SystemTime::now(); Self { session_id, @@ -308,6 +319,7 @@ pub struct SessionSummary { pub session_id: String, pub session_name: String, /// Current/default mode selection for the session. + #[serde(deserialize_with = "openbitfun_core_types::agent_identity::deserialize_agent_id")] pub agent_type: String, /// Runtime-owned model selector currently bound to the session. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -318,9 +330,15 @@ pub struct SessionSummary { pub reasoning_preset: Option, /// Mode of the last surviving user dialog turn in the session history. #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde( + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_optional_agent_id" + )] pub last_user_dialog_agent_type: Option, /// Mode of the most recent user submission accepted by the scheduler. #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde( + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_optional_agent_id" + )] pub last_submitted_agent_type: Option, #[serde( default, @@ -349,11 +367,17 @@ pub struct PersistedSessionStateFile { /// Derived runtime cache for reminder semantics. The source of truth lives /// on persisted dialog turns via `DialogTurnData.agent_type`. #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde( + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_optional_agent_id" + )] pub last_user_dialog_agent_type: Option, /// Session-level prompt-cache guard state. This records the most recent user /// submission accepted by the scheduler and intentionally does not rewind on /// history rollback. #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde( + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_optional_agent_id" + )] pub last_submitted_agent_type: Option, pub compression_state: CompressionState, pub runtime_state: SessionState, @@ -428,7 +452,8 @@ mod tests { .remove("prompt_cache_lineage_id"); let legacy_config: SessionConfig = serde_json::from_value(legacy_value).expect("deserialize legacy config"); - let mut session = Session::new("Session".to_string(), "agentic".to_string(), legacy_config); + let mut session = + Session::new("Session".to_string(), "Standard".to_string(), legacy_config); assert!(session.config.prompt_cache_lineage_id.is_none()); assert_eq!( session.effective_prompt_cache_lineage_id(), @@ -544,12 +569,12 @@ mod tests { fn new_session_preserves_legacy_runtime_defaults() { let session = Session::new( "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig::default(), ); assert_eq!(session.session_name, "Session"); - assert_eq!(session.agent_type, "agentic"); + assert_eq!(session.agent_type, "Standard"); assert_eq!(session.dialog_turn_ids, Vec::::new()); assert_eq!(session.state, SessionState::Idle); assert_eq!(session.compression_state.compression_count, 0); @@ -573,7 +598,7 @@ mod tests { let session = Session::new_with_id( "session_1".to_string(), "Main".to_string(), - "agentic".to_string(), + "Standard".to_string(), SessionConfig { model_id: Some("provider/model".to_string()), workspace_path: Some("/worktrees/session_1".to_string()), @@ -588,7 +613,7 @@ mod tests { assert_eq!(result.session_id, "session_1"); assert_eq!(result.session_name, "Main"); - assert_eq!(result.agent_type, "agentic"); + assert_eq!(result.agent_type, "Standard"); assert_eq!(result.model_id.as_deref(), Some("provider/model")); assert_eq!( result.workspace_path.as_deref(), @@ -632,7 +657,7 @@ mod tests { ..SessionConfig::default() }, snapshot_session_id: Some("snapshot-1".to_string()), - last_user_dialog_agent_type: Some("agentic".to_string()), + last_user_dialog_agent_type: Some("Standard".to_string()), last_submitted_agent_type: Some("DeepReview".to_string()), compression_state: CompressionState { last_compression_at: None, @@ -656,7 +681,7 @@ mod tests { "model_id": "model-a" }, "snapshot_session_id": "snapshot-1", - "last_user_dialog_agent_type": "agentic", + "last_user_dialog_agent_type": "Standard", "last_submitted_agent_type": "DeepReview", "compression_state": { "last_compression_at": null, diff --git a/src/crates/execution/agent-runtime/src/session_control.rs b/src/crates/execution/agent-runtime/src/session_control.rs index 4a60a302a3..01d5619f79 100644 --- a/src/crates/execution/agent-runtime/src/session_control.rs +++ b/src/crates/execution/agent-runtime/src/session_control.rs @@ -28,7 +28,7 @@ impl SessionControlAction { #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub enum SessionControlAgentType { - #[serde(rename = "agentic", alias = "Agentic", alias = "AGENTIC")] + #[serde(rename = "Standard", alias = "Standard", alias = "AGENTIC")] Agentic, #[serde(rename = "Cowork", alias = "cowork", alias = "COWORK")] Cowork, @@ -43,7 +43,7 @@ pub enum SessionControlAgentType { impl SessionControlAgentType { pub const fn as_str(&self) -> &'static str { match self { - Self::Agentic => "agentic", + Self::Agentic => "Standard", Self::Cowork => "Cowork", Self::DeepResearch => "DeepResearch", } @@ -131,7 +131,7 @@ pub fn session_control_agent_type_or_default( ) -> String { agent_type .map(|agent_type| agent_type.as_str().to_string()) - .unwrap_or_else(|| "agentic".to_string()) + .unwrap_or_else(|| "Standard".to_string()) } pub fn session_control_creator_marker(creator_session_id: &str) -> String { diff --git a/src/crates/execution/agent-runtime/src/skills/policy.rs b/src/crates/execution/agent-runtime/src/skills/policy.rs index ee4114d6de..bdcc5ae2dc 100644 --- a/src/crates/execution/agent-runtime/src/skills/policy.rs +++ b/src/crates/execution/agent-runtime/src/skills/policy.rs @@ -1,5 +1,5 @@ use super::catalog::{builtin_skill_spec, BuiltinSkillGroup, BuiltinSkillSpec}; -use crate::agents::{resolve_mode_config_profile_id, SHARED_CODING_MODE_CONFIG_PROFILE_ID}; +use crate::agents::{resolve_mode_config_profile_id, STANDARD_HARNESS_CONFIG_ID}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum SkillModeId { @@ -17,13 +17,13 @@ enum SkillModeId { impl SkillModeId { fn parse(mode_id: &str) -> Self { match mode_id.trim() { - SHARED_CODING_MODE_CONFIG_PROFILE_ID => Self::CodingShared, + STANDARD_HARNESS_CONFIG_ID => Self::CodingShared, "Cowork" => Self::Cowork, "Claw" => Self::Claw, "Creative" => Self::Creative, "ComputerUse" => Self::ComputerUse, "DeepResearch" => Self::DeepResearch, - "Ultra" => Self::Ultra, + "Ultimate" => Self::Ultra, "SwarmWorker" => Self::SwarmWorker, _ => Self::Other, } @@ -239,14 +239,14 @@ pub fn resolve_builtin_default_enabled(dir_name: &str, mode_id: &str) -> Option< #[cfg(test)] mod tests { use super::*; - use crate::agents::{SHARED_CODING_MODE_CONFIG_PROFILE_ID, SHARED_CODING_MODE_IDS}; + use crate::agents::{STANDARD_HARNESS_CONFIG_ID, STANDARD_HARNESS_CONFIG_MEMBERS}; use crate::skills::catalog::BUILTIN_SKILL_SPECS; #[test] fn agent_browser_defaults_on_only_for_ultra_and_swarm_worker() { for mode_id in [ - "agentic", - "coding_shared", + "Standard", + "Standard", "Claw", "Creative", "Cowork", @@ -264,7 +264,7 @@ mod tests { ); } - for mode_id in ["Ultra", "SwarmWorker"] { + for mode_id in ["Ultimate", "SwarmWorker"] { assert_eq!( resolve_builtin_default_enabled("agent-browser", mode_id), Some(true), @@ -277,7 +277,7 @@ mod tests { fn swarm_agents_expose_only_their_requested_builtin_skills_by_default() { for spec in BUILTIN_SKILL_SPECS { assert_eq!( - resolve_builtin_default_enabled(spec.dir_name, "Ultra"), + resolve_builtin_default_enabled(spec.dir_name, "Ultimate"), Some(matches!(spec.dir_name, "plan" | "agent-browser")), "Ultra has unexpected default exposure for {}", spec.dir_name @@ -300,14 +300,14 @@ mod tests { "pr-review-canvas", ] { for mode_id in [ - "agentic", - "coding_shared", + "Standard", + "Standard", "Claw", "Creative", "Cowork", "ComputerUse", "DeepResearch", - "Ultra", + "Ultimate", "SwarmWorker", "SomeUnknownMode", ] { @@ -323,11 +323,9 @@ mod tests { #[test] fn shared_coding_modes_use_their_profile_builtin_skill_defaults() { for spec in BUILTIN_SKILL_SPECS { - let expected = resolve_builtin_default_enabled( - spec.dir_name, - SHARED_CODING_MODE_CONFIG_PROFILE_ID, - ); - for mode_id in SHARED_CODING_MODE_IDS { + let expected = + resolve_builtin_default_enabled(spec.dir_name, STANDARD_HARNESS_CONFIG_ID); + for mode_id in STANDARD_HARNESS_CONFIG_MEMBERS { assert_eq!( resolve_builtin_default_enabled(spec.dir_name, mode_id), expected, @@ -341,7 +339,7 @@ mod tests { #[test] fn debug_skill_defaults_on_only_in_agentic() { - for mode_id in ["agentic", "coding_shared"] { + for mode_id in ["Standard", "Standard"] { assert_eq!( resolve_builtin_default_enabled("debug", mode_id), Some(true), @@ -355,7 +353,7 @@ mod tests { "Cowork", "ComputerUse", "DeepResearch", - "Ultra", + "Ultimate", "SwarmWorker", "Team", "SomeUnknownMode", @@ -371,8 +369,8 @@ mod tests { #[test] fn multitask_skill_replaces_multitask_mode_in_coding_workflows() { for mode_id in [ - "agentic", - "coding_shared", + "Standard", + "Standard", "Claw", "Cowork", "Creative", @@ -385,7 +383,7 @@ mod tests { ); } - for mode_id in ["ComputerUse", "Ultra", "SwarmWorker", "SomeUnknownMode"] { + for mode_id in ["ComputerUse", "Ultimate", "SwarmWorker", "SomeUnknownMode"] { assert_eq!( resolve_builtin_default_enabled("multitask", mode_id), Some(false), @@ -397,12 +395,7 @@ mod tests { #[test] fn plan_skill_defaults_on_for_planning_capable_workflows() { for mode_id in [ - "agentic", - "coding_shared", - "Claw", - "Cowork", - "Creative", - "Ultra", + "Standard", "Standard", "Claw", "Cowork", "Creative", "Ultimate", ] { assert_eq!( resolve_builtin_default_enabled("plan", mode_id), @@ -431,14 +424,14 @@ mod tests { fn product_creation_skills_default_only_in_creative_mode() { for skill in ["miniapp-dev", "openbitfun-frontend-dev"] { for mode_id in [ - "agentic", - "coding_shared", + "Standard", + "Standard", "Claw", "Creative", "Cowork", "ComputerUse", "DeepResearch", - "Ultra", + "Ultimate", "SwarmWorker", "SomeUnknownMode", ] { diff --git a/src/crates/execution/agent-runtime/tests/agent_definition_contracts/agent_registry_contracts.rs b/src/crates/execution/agent-runtime/tests/agent_definition_contracts/agent_registry_contracts.rs index 04e8d90973..7ae1c0268f 100644 --- a/src/crates/execution/agent-runtime/tests/agent_definition_contracts/agent_registry_contracts.rs +++ b/src/crates/execution/agent-runtime/tests/agent_definition_contracts/agent_registry_contracts.rs @@ -3,11 +3,10 @@ use openbitfun_agent_runtime::agents::{ is_swarm_delegate_agent_type, is_swarm_planner_agent_type, mode_config_profile_label, mode_config_profile_member_mode_ids, mode_presentation_rank, resolve_mode_config_profile_id, resolve_subagent_availability, resolve_subagent_default_enabled, - shared_coding_mode_user_context_policy, subagent_source_kind, - subagent_source_presentation_rank, BuiltinAgentCategory, BuiltinSubagentExposure, - SubAgentSource, SubagentOverrideLayers, SubagentOverrideState, SubagentSourceKind, - SubagentStateReason, SubagentVisibilityPolicy, SHARED_CODING_MODE_CONFIG_PROFILE_ID, - SHARED_CODING_MODE_CONFIG_PROFILE_LABEL, SHARED_CODING_MODE_IDS, + standard_harness_user_context_policy, subagent_source_kind, subagent_source_presentation_rank, + BuiltinAgentCategory, BuiltinSubagentExposure, SubAgentSource, SubagentOverrideLayers, + SubagentOverrideState, SubagentSourceKind, SubagentStateReason, SubagentVisibilityPolicy, + STANDARD_HARNESS_CONFIG_ID, STANDARD_HARNESS_CONFIG_LABEL, STANDARD_HARNESS_CONFIG_MEMBERS, }; use openbitfun_agent_runtime::deep_review::canonical_review_worker_agent_type; @@ -15,16 +14,16 @@ use openbitfun_agent_runtime::deep_review::canonical_review_worker_agent_type; fn visibility_policy_supports_public_restricted_hidden_and_denied_parents() { let public = SubagentVisibilityPolicy::public(); assert!(public.can_access_from_parent(None)); - assert!(public.can_access_from_parent(Some("agentic"))); + assert!(public.can_access_from_parent(Some("Standard"))); let restricted = SubagentVisibilityPolicy::restricted(["DeepResearch"]); assert!(!restricted.can_access_from_parent(None)); assert!(restricted.can_access_from_parent(Some("DeepResearch"))); - assert!(!restricted.can_access_from_parent(Some("agentic"))); + assert!(!restricted.can_access_from_parent(Some("Standard"))); let denied = SubagentVisibilityPolicy::public().deny_for(["BlockedParent"]); assert!(!denied.can_access_from_parent(Some("BlockedParent"))); - assert!(denied.can_access_from_parent(Some("agentic"))); + assert!(denied.can_access_from_parent(Some("Standard"))); let hidden = SubagentVisibilityPolicy::hidden(["DeepReview"]); assert_eq!(hidden.summary().exposure, BuiltinSubagentExposure::Hidden); @@ -34,7 +33,7 @@ fn visibility_policy_supports_public_restricted_hidden_and_denied_parents() { #[test] fn swarm_agent_type_contract_is_closed() { - for parent in ["Ultra", "SwarmPlanner"] { + for parent in ["Ultimate", "SwarmPlanner"] { assert!(is_swarm_planner_agent_type(parent)); } assert!(!is_swarm_planner_agent_type("GeneralPurpose")); @@ -99,7 +98,7 @@ fn default_enabled_uses_visibility_only_for_builtin_subagents() { assert!(!resolve_subagent_default_enabled( SubagentSourceKind::Builtin, &hidden, - Some("agentic") + Some("Standard") )); assert!(resolve_subagent_default_enabled( SubagentSourceKind::Builtin, @@ -109,32 +108,32 @@ fn default_enabled_uses_visibility_only_for_builtin_subagents() { assert!(resolve_subagent_default_enabled( SubagentSourceKind::Project, &hidden, - Some("agentic") + Some("Standard") )); assert!(resolve_subagent_default_enabled( SubagentSourceKind::User, &hidden, - Some("agentic") + Some("Standard") )); } #[test] fn shared_coding_modes_resolve_to_the_same_config_profile() { - for mode_id in SHARED_CODING_MODE_IDS { + for mode_id in STANDARD_HARNESS_CONFIG_MEMBERS { assert_eq!( resolve_mode_config_profile_id(mode_id).as_ref(), - SHARED_CODING_MODE_CONFIG_PROFILE_ID + STANDARD_HARNESS_CONFIG_ID ); } assert_eq!(resolve_mode_config_profile_id("Cowork").as_ref(), "Cowork"); assert_eq!( - mode_config_profile_member_mode_ids(SHARED_CODING_MODE_CONFIG_PROFILE_ID), - SHARED_CODING_MODE_IDS + mode_config_profile_member_mode_ids(STANDARD_HARNESS_CONFIG_ID), + STANDARD_HARNESS_CONFIG_MEMBERS ); assert_eq!( - mode_config_profile_label(SHARED_CODING_MODE_CONFIG_PROFILE_ID), - Some(SHARED_CODING_MODE_CONFIG_PROFILE_LABEL) + mode_config_profile_label(STANDARD_HARNESS_CONFIG_ID), + Some(STANDARD_HARNESS_CONFIG_LABEL) ); } @@ -179,14 +178,14 @@ fn subagent_source_contract_preserves_runtime_kind_and_presentation_order() { #[test] fn mode_presentation_and_shared_context_policy_match_existing_mode_contract() { - assert_eq!(mode_presentation_rank("agentic"), 0); + assert_eq!(mode_presentation_rank("Standard"), 0); assert_eq!(mode_presentation_rank("Cowork"), 1); - assert_eq!(mode_presentation_rank("Ultra"), 3); + assert_eq!(mode_presentation_rank("Ultimate"), 3); assert_eq!(mode_presentation_rank("Creative"), 4); assert_eq!(mode_presentation_rank("unknown"), 99); assert_eq!( - shared_coding_mode_user_context_policy().cache_scope_key(), + standard_harness_user_context_policy().cache_scope_key(), "workspace_context|workspace_instructions|project_layout|memory_summary" ); } @@ -198,13 +197,13 @@ fn builtin_agent_definition_catalog_preserves_order_categories_models_and_visibi assert_eq!( ids, vec![ - "minimal", - "agentic", + "Minimal", + "Standard", "Cowork", "Creative", "Claw", "DeepResearch", - "Ultra", + "Ultimate", "SwarmPlanner", "SwarmWorker", "SwarmReviewer", @@ -235,9 +234,9 @@ fn builtin_agent_definition_catalog_preserves_order_categories_models_and_visibi assert_eq!(code_review.category, BuiltinAgentCategory::SubAgent); assert!(code_review .visibility_policy - .can_access_from_parent(Some("agentic"))); + .can_access_from_parent(Some("Standard"))); assert!(!code_review.visibility_policy.show_in_global_registry); - assert_eq!(default_model_id_for_builtin_agent("agentic"), "primary"); + assert_eq!(default_model_id_for_builtin_agent("Standard"), "primary"); assert_eq!(default_model_id_for_builtin_agent("Explore"), "primary"); assert_eq!( default_model_id_for_builtin_agent("GeneralPurpose"), @@ -258,7 +257,7 @@ fn builtin_agent_definition_catalog_preserves_order_categories_models_and_visibi ); assert_eq!(default_model_id_for_builtin_agent("ReviewGeneral"), "fast"); assert_eq!(default_model_id_for_builtin_agent("ReviewWorker"), "fast"); - assert_eq!(default_model_id_for_builtin_agent("Ultra"), "primary"); + assert_eq!(default_model_id_for_builtin_agent("Ultimate"), "primary"); assert_eq!( default_model_id_for_builtin_agent("SwarmPlanner"), "primary" @@ -273,14 +272,14 @@ fn builtin_agent_definition_catalog_preserves_order_categories_models_and_visibi .expect("Swarm agent should be registered"); assert!(swarm .visibility_policy - .can_access_from_parent(Some("Ultra"))); + .can_access_from_parent(Some("Ultimate"))); assert!(swarm .visibility_policy .can_access_from_parent(Some("SwarmPlanner"))); assert!(!swarm.visibility_policy.show_in_global_registry); assert!(!swarm .visibility_policy - .can_access_from_parent(Some("agentic"))); + .can_access_from_parent(Some("Standard"))); } let computer_use = specs @@ -296,7 +295,7 @@ fn builtin_agent_definition_catalog_preserves_order_categories_models_and_visibi .can_access_from_parent(Some("Claw"))); assert!(!computer_use .visibility_policy - .can_access_from_parent(Some("agentic"))); + .can_access_from_parent(Some("Standard"))); let research_specialist = specs .iter() @@ -337,10 +336,10 @@ fn shared_coding_modes_have_identical_builtin_subagent_defaults() { let expected = resolve_subagent_default_enabled( SubagentSourceKind::Builtin, &spec.visibility_policy, - Some("agentic"), + Some("Standard"), ); - for mode_id in SHARED_CODING_MODE_IDS { + for mode_id in STANDARD_HARNESS_CONFIG_MEMBERS { assert_eq!( resolve_subagent_default_enabled( SubagentSourceKind::Builtin, diff --git a/src/crates/execution/agent-runtime/tests/agent_definition_contracts/context_profile.rs b/src/crates/execution/agent-runtime/tests/agent_definition_contracts/context_profile.rs index 01bc9968f6..71fb48f466 100644 --- a/src/crates/execution/agent-runtime/tests/agent_definition_contracts/context_profile.rs +++ b/src/crates/execution/agent-runtime/tests/agent_definition_contracts/context_profile.rs @@ -5,7 +5,7 @@ use openbitfun_agent_runtime::context_profile::{ #[test] fn context_profile_maps_long_running_agents_to_long_task_profile() { for agent_type in [ - "agentic", + "Standard", "DeepReview", "DeepResearch", "ComputerUse", diff --git a/src/crates/execution/agent-runtime/tests/agent_definition_contracts/skill_contracts.rs b/src/crates/execution/agent-runtime/tests/agent_definition_contracts/skill_contracts.rs index 35188ef65d..9275872a1f 100644 --- a/src/crates/execution/agent-runtime/tests/agent_definition_contracts/skill_contracts.rs +++ b/src/crates/execution/agent-runtime/tests/agent_definition_contracts/skill_contracts.rs @@ -406,7 +406,7 @@ fn builtin_skill_catalog_and_mode_policy_are_runtime_owned() { assert_eq!(builtin_skill_group_key("unknown-skill"), None); assert_eq!( - resolve_builtin_default_enabled("ppt-design", "agentic"), + resolve_builtin_default_enabled("ppt-design", "Standard"), Some(false) ); assert_eq!( @@ -422,7 +422,7 @@ fn builtin_skill_catalog_and_mode_policy_are_runtime_owned() { Some(true) ); assert_eq!( - resolve_builtin_default_enabled("miniapp-dev", "agentic"), + resolve_builtin_default_enabled("miniapp-dev", "Standard"), Some(false) ); assert_eq!( @@ -442,20 +442,23 @@ fn builtin_skill_catalog_and_mode_policy_are_runtime_owned() { Some(true) ); assert_eq!( - resolve_builtin_default_enabled("openbitfun-frontend-dev", "agentic"), + resolve_builtin_default_enabled("openbitfun-frontend-dev", "Standard"), Some(false) ); assert_eq!( - resolve_builtin_default_enabled("agent-browser", "coding_shared"), + resolve_builtin_default_enabled("agent-browser", "Standard"), Some(false) ); assert_eq!( - resolve_builtin_default_enabled("agent-browser", "Ultra"), + resolve_builtin_default_enabled("agent-browser", "Ultimate"), Some(true) ); - assert_eq!(resolve_builtin_default_enabled("plan", "Ultra"), Some(true)); assert_eq!( - resolve_builtin_default_enabled("find-skills", "Ultra"), + resolve_builtin_default_enabled("plan", "Ultimate"), + Some(true) + ); + assert_eq!( + resolve_builtin_default_enabled("find-skills", "Ultimate"), Some(false) ); assert_eq!( @@ -467,7 +470,7 @@ fn builtin_skill_catalog_and_mode_policy_are_runtime_owned() { Some(false) ); for (mode_id, expected) in [ - ("coding_shared", true), + ("Standard", true), ("Cowork", true), ("Creative", true), ("DeepResearch", true), @@ -480,8 +483,8 @@ fn builtin_skill_catalog_and_mode_policy_are_runtime_owned() { ); } for (mode_id, expected) in [ - ("agentic", true), - ("coding_shared", true), + ("Standard", true), + ("Standard", true), ("Claw", true), ("Cowork", true), ("Creative", true), @@ -501,8 +504,8 @@ fn builtin_skill_catalog_and_mode_policy_are_runtime_owned() { "pr-review-canvas", ] { for mode_id in [ - "agentic", - "coding_shared", + "Standard", + "Standard", "Claw", "Cowork", "Creative", @@ -666,13 +669,13 @@ fn skill_resolution_applies_builtin_and_user_override_rules() { assert!(!resolve_skill_default_enabled_for_mode( &presentation, - "agentic" + "Standard" )); - assert!(resolve_skill_default_enabled_for_mode(&custom, "agentic")); + assert!(resolve_skill_default_enabled_for_mode(&custom, "Standard")); let default_state = resolve_skill_state_for_mode( &presentation, - "agentic", + "Standard", &UserModeSkillOverrides::default(), &disabled_project, ); @@ -685,7 +688,7 @@ fn skill_resolution_applies_builtin_and_user_override_rules() { let mut overrides = UserModeSkillOverrides::default(); overrides.enabled_skills.push(presentation.key.clone()); let enabled_state = - resolve_skill_state_for_mode(&presentation, "agentic", &overrides, &disabled_project); + resolve_skill_state_for_mode(&presentation, "Standard", &overrides, &disabled_project); assert!(enabled_state.effective_enabled); assert_eq!( enabled_state.reason, @@ -1053,7 +1056,7 @@ fn mode_skill_candidate_filtering_and_info_are_runtime_owned() { let filtered = filter_candidates_for_mode( vec![project_doc.clone(), custom_user.clone()], - "agentic", + "Standard", &UserModeSkillOverrides::default(), &disabled_project, ); @@ -1068,7 +1071,7 @@ fn mode_skill_candidate_filtering_and_info_are_runtime_owned() { let infos = build_mode_skill_infos( all_skills, resolved, - "agentic", + "Standard", &UserModeSkillOverrides::default(), &disabled_project, &HashSet::new(), @@ -1127,7 +1130,7 @@ fn mode_skill_info_reports_the_actual_runtime_winner_after_filtering() { let filtered = filter_candidates_for_mode( candidates.clone(), - "agentic", + "Standard", &UserModeSkillOverrides::default(), &disabled_project, ); @@ -1135,7 +1138,7 @@ fn mode_skill_info_reports_the_actual_runtime_winner_after_filtering() { let infos = build_mode_skill_infos( sort_skills(annotate_shadowed_skills(candidates)), resolved, - "agentic", + "Standard", &UserModeSkillOverrides::default(), &disabled_project, &HashSet::new(), @@ -1188,7 +1191,7 @@ fn global_skill_disable_overrides_mode_selection_without_changing_mode_defaults( let infos = build_mode_skill_infos( vec![skill], Vec::new(), - "agentic", + "Standard", &UserModeSkillOverrides::default(), &HashSet::new(), &globally_disabled, @@ -1206,7 +1209,7 @@ fn global_skill_disable_overrides_mode_selection_without_changing_mode_defaults( let filtered = filter_candidates_for_mode( vec![candidate], - "agentic", + "Standard", &UserModeSkillOverrides::default(), &HashSet::new(), ); @@ -1223,7 +1226,7 @@ fn explicit_invocation_hidden_builtin_fallback_is_runtime_owned() { match resolve_default_hidden_builtin_for_explicit_invocation( "gstack-review", vec![candidate.clone()], - Some("agentic"), + Some("Standard"), ) { ExplicitSkillInvocationResolution::Found(skill) => { assert_eq!(skill.key, "user::openbitfun-system::gstack-review"); @@ -1235,7 +1238,7 @@ fn explicit_invocation_hidden_builtin_fallback_is_runtime_owned() { resolve_default_hidden_builtin_for_explicit_invocation( "missing-skill", vec![candidate.clone()], - Some("agentic") + Some("Standard") ), ExplicitSkillInvocationResolution::NotFound )); @@ -1258,7 +1261,7 @@ fn explicit_invocation_reaches_default_hidden_agent_browser() { priority: 10, }; - for mode_id in ["agentic", "coding_shared", "Claw", "Cowork"] { + for mode_id in ["Standard", "Standard", "Claw", "Cowork"] { assert_eq!( resolve_builtin_default_enabled("agent-browser", mode_id), Some(false), diff --git a/src/crates/execution/agent-runtime/tests/agent_interaction_contracts/permission_contracts.rs b/src/crates/execution/agent-runtime/tests/agent_interaction_contracts/permission_contracts.rs index 54ae74d494..0332ed81c0 100644 --- a/src/crates/execution/agent-runtime/tests/agent_interaction_contracts/permission_contracts.rs +++ b/src/crates/execution/agent-runtime/tests/agent_interaction_contracts/permission_contracts.rs @@ -473,7 +473,7 @@ fn request(request_id: &str, session_id: &str) -> PermissionRequest { project_path: None, project_id: "project-a".to_string(), session_id: session_id.to_string(), - agent_id: "agentic".to_string(), + agent_id: "Standard".to_string(), action: "edit".to_string(), resources: vec!["src/lib.rs".to_string()], save_resources: vec!["src/*".to_string(), "src/*".to_string()], diff --git a/src/crates/execution/agent-runtime/tests/agent_session_contracts/scheduler_contracts.rs b/src/crates/execution/agent-runtime/tests/agent_session_contracts/scheduler_contracts.rs index 8494089372..74c7659bbd 100644 --- a/src/crates/execution/agent-runtime/tests/agent_session_contracts/scheduler_contracts.rs +++ b/src/crates/execution/agent-runtime/tests/agent_session_contracts/scheduler_contracts.rs @@ -353,7 +353,7 @@ fn active_dialog_turn_owns_agent_session_reply_suppression_facts() { Some("workspace".to_string()), Some("target-conn".to_string()), Some("target-host".to_string()), - "agentic".to_string(), + "Standard".to_string(), "run task".to_string(), Some(serde_json::json!({"kind": "session_message"})), DialogSubmissionPolicy::for_source(DialogTriggerSource::AgentSession), @@ -365,7 +365,7 @@ fn active_dialog_turn_owns_agent_session_reply_suppression_facts() { assert_eq!(turn.workspace_path(), Some("workspace")); assert_eq!(turn.remote_connection_id(), Some("target-conn")); assert_eq!(turn.remote_ssh_host(), Some("target-host")); - assert_eq!(turn.agent_type(), "agentic"); + assert_eq!(turn.agent_type(), "Standard"); assert_eq!(turn.user_input(), "run task"); assert!(turn.user_message_metadata().is_some()); assert!(turn.reply_route().is_some()); @@ -380,7 +380,7 @@ fn active_dialog_turn_does_not_suppress_non_agent_session_turns() { None, None, None, - "agentic".to_string(), + "Standard".to_string(), "user task".to_string(), None, DialogSubmissionPolicy::for_source(DialogTriggerSource::DesktopUi), @@ -491,7 +491,7 @@ fn agent_session_reply_action_ignores_non_agent_session_turns() { Some("workspace".to_string()), None, None, - "agentic".to_string(), + "Standard".to_string(), "user task".to_string(), None, DialogSubmissionPolicy::for_source(DialogTriggerSource::DesktopUi), @@ -704,7 +704,7 @@ fn agent_session_turn(source_session_id: &str) -> ActiveDialogTurn { Some("workspace".to_string()), Some("target-conn".to_string()), Some("target-host".to_string()), - "agentic".to_string(), + "Standard".to_string(), "run task".to_string(), Some(serde_json::json!({"kind": "session_message"})), DialogSubmissionPolicy::for_source(DialogTriggerSource::AgentSession), diff --git a/src/crates/execution/agent-runtime/tests/agent_session_contracts/sdk_smoke.rs b/src/crates/execution/agent-runtime/tests/agent_session_contracts/sdk_smoke.rs index d8e063d9ea..23038cdf49 100644 --- a/src/crates/execution/agent-runtime/tests/agent_session_contracts/sdk_smoke.rs +++ b/src/crates/execution/agent-runtime/tests/agent_session_contracts/sdk_smoke.rs @@ -282,7 +282,7 @@ impl AgentSubmissionPort for FakeSdkAgentProvider { } async fn resolve_session_agent_type(&self, _session_id: &str) -> PortResult> { - Ok(Some("agentic".to_string())) + Ok(Some("Standard".to_string())) } } @@ -301,7 +301,7 @@ async fn sdk_facade_runs_with_fake_provider_and_local_event_stream() { AgentRunRequest::new( SessionSelector::create( "SDK smoke", - "agentic", + "Standard", Some("/workspace/project".to_string()), ), "hello from sdk", @@ -314,7 +314,7 @@ async fn sdk_facade_runs_with_fake_provider_and_local_event_stream() { assert_eq!(handle.session_id, "sdk-session-1"); assert_eq!(handle.turn_id, "sdk-turn-1"); - assert_eq!(handle.agent_type.as_deref(), Some("agentic")); + assert_eq!(handle.agent_type.as_deref(), Some("Standard")); assert!(handle.accepted); runtime @@ -410,9 +410,9 @@ async fn sdk_facade_accepts_fake_services_tools_and_hooks_without_core() { .with_tool_registry(Arc::new(tools)) .with_hook_registry(hooks) .with_agent_registry(Arc::new(FakeSdkAgentRegistry { - agent_ids: vec!["agentic".to_string(), "Explore".to_string()], + agent_ids: vec!["Standard".to_string(), "Explore".to_string()], workspace_agent_ids: vec![ - "agentic".to_string(), + "Standard".to_string(), "Explore".to_string(), "ProjectReviewer".to_string(), ], @@ -424,14 +424,14 @@ async fn sdk_facade_accepts_fake_services_tools_and_hooks_without_core() { assert_eq!(runtime.hook_registry().hooks()[0].id(), "sdk.post_call"); assert_eq!( runtime.registered_agent_ids(RuntimeAgentRegistryQuery::default()), - vec!["agentic".to_string(), "Explore".to_string()] + vec!["Standard".to_string(), "Explore".to_string()] ); assert_eq!( runtime.registered_agent_ids(RuntimeAgentRegistryQuery { workspace_root: Some(Path::new("/workspace/project")), }), vec![ - "agentic".to_string(), + "Standard".to_string(), "Explore".to_string(), "ProjectReviewer".to_string() ] diff --git a/src/crates/execution/agent-runtime/tests/agent_session_contracts/session_model_sdk.rs b/src/crates/execution/agent-runtime/tests/agent_session_contracts/session_model_sdk.rs index c58076402e..fc83819772 100644 --- a/src/crates/execution/agent-runtime/tests/agent_session_contracts/session_model_sdk.rs +++ b/src/crates/execution/agent-runtime/tests/agent_session_contracts/session_model_sdk.rs @@ -36,7 +36,7 @@ impl AgentSubmissionPort for FakeSubmissionPort { } async fn resolve_session_agent_type(&self, _session_id: &str) -> PortResult> { - Ok(Some("agentic".to_string())) + Ok(Some("Standard".to_string())) } } diff --git a/src/crates/execution/agent-runtime/tests/agent_session_contracts/session_operation_ports.rs b/src/crates/execution/agent-runtime/tests/agent_session_contracts/session_operation_ports.rs index 6c1e01f55f..1abd59927b 100644 --- a/src/crates/execution/agent-runtime/tests/agent_session_contracts/session_operation_ports.rs +++ b/src/crates/execution/agent-runtime/tests/agent_session_contracts/session_operation_ports.rs @@ -36,7 +36,7 @@ impl AgentSubmissionPort for FakeSubmissionPort { } async fn resolve_session_agent_type(&self, _session_id: &str) -> PortResult> { - Ok(Some("agentic".to_string())) + Ok(Some("Standard".to_string())) } } @@ -58,7 +58,7 @@ impl AgentSessionForkPort for RecordingSessionOperations { Ok(AgentSessionForkResult { session_id: "session-2".to_string(), session_name: "Main (fork)".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), }) } @@ -74,7 +74,7 @@ impl AgentSessionForkPort for RecordingSessionOperations { Ok(AgentSessionForkResult { session_id: "session-2".to_string(), session_name: "Main (fork)".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), }) } } diff --git a/src/crates/execution/tool-contracts/tests/tool_contracts.rs b/src/crates/execution/tool-contracts/tests/tool_contracts.rs index fae01a96d9..9da730ecf6 100644 --- a/src/crates/execution/tool-contracts/tests/tool_contracts.rs +++ b/src/crates/execution/tool-contracts/tests/tool_contracts.rs @@ -869,7 +869,7 @@ fn delegation_restrictions_cover_all_agent_spawn_surfaces() { fn tool_context_facts_keep_portable_wire_shape_without_runtime_handles() { let facts = ToolContextFacts { tool_call_id: Some("call-1".to_string()), - agent_type: Some("Agentic".to_string()), + agent_type: Some("Standard".to_string()), session_id: Some("session-1".to_string()), dialog_turn_id: Some("turn-1".to_string()), workspace_kind: Some(ToolWorkspaceKind::Remote), @@ -880,7 +880,7 @@ fn tool_context_facts_keep_portable_wire_shape_without_runtime_handles() { let value = serde_json::to_value(&facts).expect("serialize context facts"); assert_eq!(value["toolCallId"], "call-1"); - assert_eq!(value["agentType"], "Agentic"); + assert_eq!(value["agentType"], "Standard"); assert_eq!(value["sessionId"], "session-1"); assert_eq!(value["dialogTurnId"], "turn-1"); assert_eq!(value["workspaceKind"], "remote"); @@ -910,7 +910,7 @@ fn portable_tool_context_provider_exposes_facts_only() { let provider = FactsOnlyProvider { facts: ToolContextFacts { tool_call_id: Some("call-2".to_string()), - agent_type: Some("Agentic".to_string()), + agent_type: Some("Standard".to_string()), session_id: Some("session-2".to_string()), dialog_turn_id: None, workspace_kind: Some(ToolWorkspaceKind::Local), @@ -2811,7 +2811,7 @@ async fn contextual_manifest_resolver_preserves_runtime_visible_manifest_contrac "Git".to_string(), ], &Default::default(), - &ManifestTestContext { agent: "agentic" }, + &ManifestTestContext { agent: "Standard" }, GET_TOOL_SPEC_TOOL_NAME, ) .await; @@ -2866,7 +2866,7 @@ async fn contextual_manifest_resolver_preserves_runtime_visible_manifest_contrac .find(|tool| tool.name == "Read") .expect("expanded Read manifest"); assert_eq!(read.description, "Read description for agentic"); - assert_eq!(read.parameters["properties"]["agent"]["const"], "agentic"); + assert_eq!(read.parameters["properties"]["agent"]["const"], "Standard"); assert!(!manifest .tool_definitions @@ -2893,7 +2893,7 @@ async fn contextual_manifest_resolver_accepts_snapshot_provider_boundary() { "Git".to_string(), ], &Default::default(), - &ManifestTestContext { agent: "agentic" }, + &ManifestTestContext { agent: "Standard" }, GET_TOOL_SPEC_TOOL_NAME, ) .await; @@ -2948,7 +2948,7 @@ async fn tool_catalog_runtime_facade_owns_manifest_and_readonly_paths() { "Git".to_string(), ], &Default::default(), - &ManifestTestContext { agent: "agentic" }, + &ManifestTestContext { agent: "Standard" }, ) .await; assert_eq!( @@ -2985,7 +2985,7 @@ async fn tool_catalog_runtime_facade_owns_manifest_and_readonly_paths() { "Git".to_string(), ], &Default::default(), - &ManifestTestContext { agent: "agentic" }, + &ManifestTestContext { agent: "Standard" }, ) .await; assert_eq!( @@ -3030,7 +3030,7 @@ async fn get_tool_spec_detail_resolver_preserves_contextual_detail_contract() { contextual_manifest_tool("WebFetch", ToolExposure::Deferred, None), contextual_manifest_tool(GET_TOOL_SPEC_TOOL_NAME, ToolExposure::Deferred, None), ]; - let context = ManifestTestContext { agent: "agentic" }; + let context = ManifestTestContext { agent: "Standard" }; let summaries = summarize_get_tool_spec_deferred_tools(&deferred_tools); assert_eq!( @@ -3061,7 +3061,7 @@ async fn get_tool_spec_detail_resolver_preserves_contextual_detail_contract() { assert_eq!(detail.description, "WebFetch description for agentic"); assert_eq!( detail.input_schema["properties"]["agent"]["const"], - "agentic" + "Standard" ); assert_eq!( detail.to_value(), @@ -3072,7 +3072,7 @@ async fn get_tool_spec_detail_resolver_preserves_contextual_detail_contract() { "type": "object", "properties": { "agent": { - "const": "agentic" + "const": "Standard" } } }, @@ -3107,7 +3107,7 @@ async fn get_tool_spec_catalog_provider_preserves_runtime_catalog_contract() { contextual_manifest_tool("Read", ToolExposure::Direct, None), ], }; - let context = ManifestTestContext { agent: "agentic" }; + let context = ManifestTestContext { agent: "Standard" }; let detail = resolve_get_tool_spec_detail_from_provider( &provider, @@ -3123,7 +3123,7 @@ async fn get_tool_spec_catalog_provider_preserves_runtime_catalog_contract() { #[tokio::test] async fn get_tool_spec_provider_execution_returns_duplicate_result_without_detail_lookup() { - let context = ManifestTestContext { agent: "agentic" }; + let context = ManifestTestContext { agent: "Standard" }; let input = json!({ "tool_name": "WebFetch" }); let result = resolve_get_tool_spec_execution_result_from_provider( @@ -3166,7 +3166,7 @@ async fn get_tool_spec_provider_execution_returns_detail_result_from_provider() None, )], }; - let context = ManifestTestContext { agent: "agentic" }; + let context = ManifestTestContext { agent: "Standard" }; let input = json!({ "tool_name": "WebFetch" }); let result = resolve_get_tool_spec_execution_result_from_provider( @@ -3192,7 +3192,7 @@ async fn get_tool_spec_provider_execution_returns_detail_result_from_provider() assert_eq!(data["description"], "WebFetch description for agentic"); assert_eq!( data["input_schema"]["properties"]["agent"]["const"], - "agentic" + "Standard" ); let assistant = result_for_assistant.expect("assistant detail"); assert!(assistant.contains("\nWebFetch description for agentic")); @@ -3209,7 +3209,7 @@ async fn get_tool_spec_provider_execution_returns_already_available_result_for_e contextual_manifest_tool("Read", ToolExposure::Direct, None), ], }; - let context = ManifestTestContext { agent: "agentic" }; + let context = ManifestTestContext { agent: "Standard" }; let input = json!({ "tool_name": "Read" }); let result = resolve_get_tool_spec_execution_result_from_provider( @@ -3251,7 +3251,7 @@ async fn get_tool_spec_runtime_facade_owns_execution_path() { None, )], }; - let context = ManifestTestContext { agent: "agentic" }; + let context = ManifestTestContext { agent: "Standard" }; let input = json!({ "tool_name": "WebFetch" }); let runtime = GetToolSpecRuntime::::new( &provider, @@ -3270,7 +3270,7 @@ async fn get_tool_spec_runtime_facade_owns_execution_path() { assert_eq!(data["description"], "WebFetch description for agentic"); assert_eq!( data["input_schema"]["properties"]["agent"]["const"], - "agentic" + "Standard" ); } @@ -3283,7 +3283,7 @@ async fn get_tool_spec_runtime_facade_owns_tool_result_vector_adapter_shape() { None, )], }; - let context = ManifestTestContext { agent: "agentic" }; + let context = ManifestTestContext { agent: "Standard" }; let runtime = GetToolSpecRuntime::::new( &provider, GET_TOOL_SPEC_TOOL_NAME, @@ -3382,7 +3382,7 @@ async fn get_tool_spec_provider_execution_returns_unavailable_result_for_unknown None, )], }; - let context = ManifestTestContext { agent: "agentic" }; + let context = ManifestTestContext { agent: "Standard" }; let input = json!({ "tool_name": "Git" }); let result = resolve_get_tool_spec_execution_result_from_provider( diff --git a/src/crates/interfaces/acp/src/runtime/model.rs b/src/crates/interfaces/acp/src/runtime/model.rs index 00288a4863..d81a91d4a4 100644 --- a/src/crates/interfaces/acp/src/runtime/model.rs +++ b/src/crates/interfaces/acp/src/runtime/model.rs @@ -138,11 +138,11 @@ pub(super) async fn build_session_config_options( .or_else(|| { mode_infos .iter() - .find(|mode| mode.id == "agentic") + .find(|mode| mode.id == "Standard") .or_else(|| mode_infos.first()) .map(|mode| mode.id.clone()) }) - .unwrap_or_else(|| "agentic".to_string()); + .unwrap_or_else(|| "Standard".to_string()); let mode_options = mode_infos .into_iter() .map(|mode| { diff --git a/src/crates/interfaces/acp/src/runtime/prompt.rs b/src/crates/interfaces/acp/src/runtime/prompt.rs index 1e2e5d4c20..ce2d580bbf 100644 --- a/src/crates/interfaces/acp/src/runtime/prompt.rs +++ b/src/crates/interfaces/acp/src/runtime/prompt.rs @@ -434,7 +434,7 @@ mod tests { acp_session_id: "acp-session".to_string(), openbitfun_session_id: "openbitfun-session".to_string(), cwd: "/workspace".to_string(), - mode_id: "agentic".to_string(), + mode_id: "Standard".to_string(), model_id: "primary".to_string(), mcp_server_ids: Vec::new(), lifecycle: std::sync::Arc::new(tokio::sync::Mutex::new(())), diff --git a/src/crates/interfaces/acp/src/runtime/session.rs b/src/crates/interfaces/acp/src/runtime/session.rs index eeb278e064..b289335202 100644 --- a/src/crates/interfaces/acp/src/runtime/session.rs +++ b/src/crates/interfaces/acp/src/runtime/session.rs @@ -38,9 +38,9 @@ impl OpenBitFunAcpRuntime { let cwd = request.cwd.to_string_lossy().to_string(); let mcp_servers = request.mcp_servers; self.validate_mcp_servers(&mcp_servers)?; - let modes = build_session_modes(Some("agentic")).await; + let modes = build_session_modes(Some("Standard")).await; let models = build_session_model_state(None).await?; - let config_options = build_session_config_options(None, Some("agentic")).await?; + let config_options = build_session_config_options(None, Some("Standard")).await?; let session_id = uuid::Uuid::new_v4().to_string(); Self::validate_session_target(&session_id, Path::new(&cwd))?; let _session_transition = self.claim_session_transition(&session_id)?; @@ -56,7 +56,7 @@ impl OpenBitFunAcpRuntime { "ACP Session - {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S") ), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), agent_route_key: None, workspace_path: Some(cwd.clone()), project_workspace_path: None, @@ -545,11 +545,11 @@ async fn build_session_modes(preferred_mode_id: Option<&str>) -> SessionModeStat .or_else(|| { available_modes .iter() - .find(|mode| mode.id.to_string() == "agentic") + .find(|mode| mode.id.to_string() == "Standard") .or_else(|| available_modes.first()) .map(|mode| mode.id.clone()) }) - .unwrap_or_else(|| "agentic".into()); + .unwrap_or_else(|| "Standard".into()); SessionModeState::new(current_mode_id, available_modes) } diff --git a/src/crates/interfaces/app-server/tests/agent_kernel.rs b/src/crates/interfaces/app-server/tests/agent_kernel.rs index 1cee4c8cea..ab0f2af64c 100644 --- a/src/crates/interfaces/app-server/tests/agent_kernel.rs +++ b/src/crates/interfaces/app-server/tests/agent_kernel.rs @@ -94,7 +94,7 @@ impl AgentSubmissionPort for ExampleAgentProvider { } async fn resolve_session_agent_type(&self, _session_id: &str) -> PortResult> { - Ok(Some("agentic".to_string())) + Ok(Some("Standard".to_string())) } } @@ -236,7 +236,7 @@ impl AgentSessionForkPort for SessionControlProvider { Ok(AgentSessionForkResult { session_id: format!("{}-fork", request.source_session_id), session_name: "Forked Session".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), }) } @@ -248,7 +248,7 @@ impl AgentSessionForkPort for SessionControlProvider { Ok(AgentSessionForkResult { session_id: "forked-session".to_string(), session_name: "Forked at Turn".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), }) } } @@ -264,11 +264,11 @@ impl AgentSessionRestorePort for SessionControlProvider { session: AgentSessionSummary { session_id: "session-1".to_string(), session_name: "Restored Session".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), model_id: Some("provider/model".to_string()), reasoning_preset: None, last_user_dialog_agent_type: None, - last_submitted_agent_type: Some("agentic".to_string()), + last_submitted_agent_type: Some("Standard".to_string()), turn_count: 4, created_at_ms: 10, last_active_at_ms: 20, @@ -336,7 +336,7 @@ impl AgentSubmissionPort for Phase2Provider { } async fn resolve_session_agent_type(&self, _session_id: &str) -> PortResult> { - Ok(Some("agentic".to_string())) + Ok(Some("Standard".to_string())) } } @@ -384,11 +384,11 @@ impl openbitfun_agent_runtime::sdk::AgentSessionRestorePort for Phase2Provider { session: ports::AgentSessionSummary { session_id: request.session_id, session_name: "Phase 2".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), model_id: Some("provider/model".to_string()), reasoning_preset: None, last_user_dialog_agent_type: None, - last_submitted_agent_type: Some("agentic".to_string()), + last_submitted_agent_type: Some("Standard".to_string()), turn_count: 1, created_at_ms: 10, last_active_at_ms: 20, @@ -587,7 +587,7 @@ impl ports::AgentSessionLineagePort for Phase2Provider { sessions: vec![ports::AgentSessionLineageEntry { session_id: "session-1".to_string(), session_name: "Root".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), created_at_ms: 10, status: ports::AgentSessionLifecycleStatus::Active, active_turn_id: Some("turn-active".to_string()), @@ -1330,7 +1330,7 @@ async fn run_round_trips_through_create_and_submit() { let response = recv(cx.send_request(RunMessage { session: RunSessionSpec::Create { session_name: "Example SDK Session".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: None, }, message: "hello from an app-server client".to_string(), @@ -1340,7 +1340,7 @@ async fn run_round_trips_through_create_and_submit() { .await?; assert_eq!(response.session_id, "example-session"); assert_eq!(response.turn_id, "example-turn"); - assert_eq!(response.agent_type.as_deref(), Some("agentic")); + assert_eq!(response.agent_type.as_deref(), Some("Standard")); assert!(response.accepted); Ok(()) }) @@ -1374,7 +1374,7 @@ async fn submit_dialog_turn_carries_agent_type_and_starts() { original_message: None, turn_id: None, execution: Default::default(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), workspace_path: None, remote_connection_id: None, remote_ssh_host: None, @@ -1451,7 +1451,7 @@ async fn create_session_returns_provider_session_id() { let response = recv(cx.send_request(CreateSessionMessage( AgentSessionCreateRequest { session_name: "direct create".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), agent_route_key: None, workspace_path: None, project_workspace_path: None, @@ -1466,7 +1466,7 @@ async fn create_session_returns_provider_session_id() { .await?; let CreateSessionResponse(inner) = response; assert_eq!(inner.session_id, "example-session"); - assert_eq!(inner.agent_type, "agentic"); + assert_eq!(inner.agent_type, "Standard"); Ok(()) }) .await; @@ -1750,7 +1750,7 @@ async fn client_connect_keeps_connection_alive_after_return() { let response = client .create_session(CreateSessionMessage(AgentSessionCreateRequest { session_name: "post-connect session".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), agent_route_key: None, workspace_path: None, project_workspace_path: None, @@ -1764,7 +1764,7 @@ async fn client_connect_keeps_connection_alive_after_return() { .await .expect("RPC after connect() must succeed -- connection should still be alive"); assert_eq!(response.0.session_id, "example-session"); - assert_eq!(response.0.agent_type, "agentic"); + assert_eq!(response.0.agent_type, "Standard"); client.shutdown().await; }) diff --git a/src/crates/interfaces/sdk-host/src/host.rs b/src/crates/interfaces/sdk-host/src/host.rs index 2225bad88f..257f3dc2ac 100644 --- a/src/crates/interfaces/sdk-host/src/host.rs +++ b/src/crates/interfaces/sdk-host/src/host.rs @@ -40,7 +40,7 @@ use crate::protocol::{ }; const DEFAULT_SESSION_NAME: &str = "OpenBitFun SDK query"; -const DEFAULT_AGENT: &str = "agentic"; +const DEFAULT_AGENT: &str = "Standard"; const DEFAULT_TURN_SETTLEMENT_TIMEOUT_MS: u64 = 5_000; const PERMISSION_REJECTION_TIMEOUT_MS: u64 = 2_000; const DEFAULT_PERMISSION_RESPONSE_TIMEOUT_MS: u64 = 120_000; diff --git a/src/crates/interfaces/sdk-host/src/protocol.rs b/src/crates/interfaces/sdk-host/src/protocol.rs index c23b3aac92..030271a627 100644 --- a/src/crates/interfaces/sdk-host/src/protocol.rs +++ b/src/crates/interfaces/sdk-host/src/protocol.rs @@ -754,7 +754,7 @@ mod tests { #[test] fn session_create_result_preserves_runtime_placement_facts() { - let mut created = AgentSessionCreateResult::new("session_1", "Main", "agentic"); + let mut created = AgentSessionCreateResult::new("session_1", "Main", "Standard"); created.workspace_path = Some("/worktrees/session_1".to_string()); created.workspace_id = Some("workspace_1".to_string()); created.project_workspace_path = Some("/workspace/project".to_string()); @@ -765,7 +765,7 @@ mod tests { assert_eq!(json["sessionId"], "session_1"); assert_eq!(json["sessionName"], "Main"); - assert_eq!(json["agent"], "agentic"); + assert_eq!(json["agent"], "Standard"); assert!(json.get("agentType").is_none()); assert_eq!(json["workspacePath"], "/worktrees/session_1"); assert_eq!(json["workspaceId"], "workspace_1"); diff --git a/src/crates/interfaces/sdk-host/tests/host_lifecycle.rs b/src/crates/interfaces/sdk-host/tests/host_lifecycle.rs index 8b5da9f176..09cacf9c66 100644 --- a/src/crates/interfaces/sdk-host/tests/host_lifecycle.rs +++ b/src/crates/interfaces/sdk-host/tests/host_lifecycle.rs @@ -405,7 +405,7 @@ impl AgentSubmissionPort for FakeOwner { self.release_agent_resolution.notified().await; } if self.owns_session(session_id) { - Ok(Some("agentic".to_string())) + Ok(Some("Standard".to_string())) } else { Err(PortError::new(PortErrorKind::NotFound, "session not found")) } @@ -652,7 +652,7 @@ impl AgentSessionRestorePort for FakeOwner { session: AgentSessionSummary { session_id: request.session_id, session_name: "Persisted".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), model_id: Some("sdk:openai:previous".to_string()), reasoning_preset: None, last_user_dialog_agent_type: None, @@ -817,7 +817,7 @@ fn permission_request_fixture(request_id: &str, order: u32, session_id: &str) -> project_path: Some("D:/workspace/project".to_string()), project_id: "project-fixture".to_string(), session_id: session_id.to_string(), - agent_id: "agentic".to_string(), + agent_id: "Standard".to_string(), action: "edit".to_string(), resources: vec!["src/lib.rs".to_string()], save_resources: Vec::new(), diff --git a/src/crates/services/services-core/src/session/lineage.rs b/src/crates/services/services-core/src/session/lineage.rs index 8c68409e5d..a166f006c0 100644 --- a/src/crates/services/services-core/src/session/lineage.rs +++ b/src/crates/services/services-core/src/session/lineage.rs @@ -502,7 +502,7 @@ mod tests { SessionMetadata::new( session_id.to_string(), format!("Session {session_id}"), - "agentic".to_string(), + "Standard".to_string(), "model".to_string(), ) } @@ -878,7 +878,7 @@ mod tests { source_metadata: &source, target_session_id: "target".to_string(), target_session_name: "Target".to_string(), - target_agent_type: "agentic".to_string(), + target_agent_type: "Standard".to_string(), source_session_id: "source", source_turn_id: "turn-2", source_turn_index: 1, diff --git a/src/crates/services/services-core/src/session/metadata.rs b/src/crates/services/services-core/src/session/metadata.rs index f659ee4040..3820091373 100644 --- a/src/crates/services/services-core/src/session/metadata.rs +++ b/src/crates/services/services-core/src/session/metadata.rs @@ -502,7 +502,7 @@ mod tests { SessionMetadata::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), "model".to_string(), ) } @@ -625,7 +625,7 @@ mod tests { let built = build_session_metadata(SessionMetadataBuildFacts { session_id: "session-1", session_name: "Updated session", - agent_type: "agentic", + agent_type: "Standard", last_user_dialog_agent_type: Some("plan"), last_submitted_agent_type: Some("code"), created_by: Some("creator"), @@ -682,7 +682,7 @@ mod tests { let built = build_session_metadata(SessionMetadataBuildFacts { session_id: "session-1", session_name: "New session", - agent_type: "agentic", + agent_type: "Standard", last_user_dialog_agent_type: None, last_submitted_agent_type: None, created_by: None, @@ -720,7 +720,7 @@ mod tests { let built = build_session_metadata(SessionMetadataBuildFacts { session_id: "session-worktree", session_name: "Isolated session", - agent_type: "agentic", + agent_type: "Standard", last_user_dialog_agent_type: None, last_submitted_agent_type: None, created_by: None, diff --git a/src/crates/services/services-core/src/session/metadata_store.rs b/src/crates/services/services-core/src/session/metadata_store.rs index 914ee146c9..731cdfd849 100644 --- a/src/crates/services/services-core/src/session/metadata_store.rs +++ b/src/crates/services/services-core/src/session/metadata_store.rs @@ -624,7 +624,7 @@ mod tests { let mut metadata = SessionMetadata::new( session_id.to_string(), format!("Session {session_id}"), - "agentic".to_string(), + "Standard".to_string(), "model".to_string(), ); metadata.last_active_at = last_active_at; diff --git a/src/crates/services/services-core/src/session/types.rs b/src/crates/services/services-core/src/session/types.rs index 9e74daf813..52e44c37a6 100644 --- a/src/crates/services/services-core/src/session/types.rs +++ b/src/crates/services/services-core/src/session/types.rs @@ -93,7 +93,10 @@ pub struct SessionMetadata { pub session_name: String, /// Agent type - #[serde(alias = "agent_type")] + #[serde( + alias = "agent_type", + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_agent_id" + )] pub agent_type: String, /// Mode of the last surviving user dialog turn in the persisted history. /// @@ -104,6 +107,9 @@ pub struct SessionMetadata { skip_serializing_if = "Option::is_none", alias = "last_user_dialog_agent_type" )] + #[serde( + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_optional_agent_id" + )] pub last_user_dialog_agent_type: Option, /// Mode of the most recent user submission accepted by the scheduler. /// @@ -114,6 +120,9 @@ pub struct SessionMetadata { skip_serializing_if = "Option::is_none", alias = "last_submitted_agent_type" )] + #[serde( + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_optional_agent_id" + )] pub last_submitted_agent_type: Option, /// Creator identity for future permission checks @@ -525,6 +534,9 @@ pub struct DialogTurnData { /// Maintenance/local utility turns leave this empty so they do not affect /// mode-transition reminder semantics. #[serde(default, skip_serializing_if = "Option::is_none", alias = "agent_type")] + #[serde( + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_optional_agent_id" + )] pub agent_type: Option, /// User message @@ -1142,7 +1154,8 @@ impl SessionMetadata { Self { session_id, session_name, - agent_type, + agent_type: openbitfun_core_types::agent_identity::canonical_agent_id(&agent_type) + .to_owned(), last_user_dialog_agent_type: None, last_submitted_agent_type: None, created_by: None, @@ -1494,7 +1507,7 @@ mod tests { let metadata = SessionMetadata::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), "model".to_string(), ); assert_eq!(metadata.memory_mode, SessionMemoryMode::Enabled); @@ -1502,7 +1515,7 @@ mod tests { let payload = serde_json::json!({ "sessionId": "session-1", "sessionName": "Legacy", - "agentType": "agentic", + "agentType": "Standard", "sessionKind": "standard", "modelName": "model", "createdAt": 1, @@ -1523,7 +1536,7 @@ mod tests { let mut metadata = SessionMetadata::new( "session-1".to_string(), "Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), "model".to_string(), ); metadata.memory_mode = mode; @@ -1623,7 +1636,7 @@ mod tests { let metadata = SessionMetadata::new( "session-1".to_string(), "Normal Session".to_string(), - "agentic".to_string(), + "Standard".to_string(), "model".to_string(), ); diff --git a/src/crates/services/services-core/tests/permission_store_contracts.rs b/src/crates/services/services-core/tests/permission_store_contracts.rs index afb4aa1301..d8438a2ab9 100644 --- a/src/crates/services/services-core/tests/permission_store_contracts.rs +++ b/src/crates/services/services-core/tests/permission_store_contracts.rs @@ -18,7 +18,7 @@ fn request(request_id: &str, project_id: &str) -> PermissionRequest { project_path: None, project_id: project_id.to_string(), session_id: "session-1".to_string(), - agent_id: "agentic".to_string(), + agent_id: "Standard".to_string(), action: "read".to_string(), resources: vec!["README.md".to_string()], save_resources: vec!["README.md".to_string()], diff --git a/src/crates/services/services-core/tests/session_contracts/session_contracts.rs b/src/crates/services/services-core/tests/session_contracts/session_contracts.rs index 1f35bef4d4..5cae6f2d7a 100644 --- a/src/crates/services/services-core/tests/session_contracts/session_contracts.rs +++ b/src/crates/services/services-core/tests/session_contracts/session_contracts.rs @@ -19,7 +19,7 @@ fn session_metadata_hides_ephemeral_child_sessions_from_user_lists() { let mut metadata = SessionMetadata::new( "session-ephemeral".to_string(), "Side thread".to_string(), - "agentic".to_string(), + "Standard".to_string(), "model".to_string(), ); metadata.session_kind = SessionKind::EphemeralChild; diff --git a/src/crates/services/services-core/tests/session_contracts/session_metadata_contracts.rs b/src/crates/services/services-core/tests/session_contracts/session_metadata_contracts.rs index 664faf3da0..5bc9372c4e 100644 --- a/src/crates/services/services-core/tests/session_contracts/session_metadata_contracts.rs +++ b/src/crates/services/services-core/tests/session_contracts/session_metadata_contracts.rs @@ -15,6 +15,28 @@ fn metadata(session_id: &str) -> SessionMetadata { ) } +#[test] +fn legacy_agent_identity_migrates_without_changing_session_history() { + let mut old = serde_json::to_value(metadata("legacy-session")).unwrap(); + old["agentType"] = serde_json::json!("agentic"); + old["lastUserDialogAgentType"] = serde_json::json!("Ultra"); + old["lastSubmittedAgentType"] = serde_json::json!("minimal"); + let migrated: SessionMetadata = serde_json::from_value(old.clone()).unwrap(); + assert_eq!(migrated.agent_type, "Standard"); + assert_eq!( + migrated.last_user_dialog_agent_type.as_deref(), + Some("Ultimate") + ); + assert_eq!( + migrated.last_submitted_agent_type.as_deref(), + Some("Minimal") + ); + old["agentType"] = serde_json::json!("Standard"); + old["lastUserDialogAgentType"] = serde_json::json!("Ultimate"); + old["lastSubmittedAgentType"] = serde_json::json!("Minimal"); + assert_eq!(serde_json::to_value(migrated).unwrap(), old); +} + fn user_message(content: &str) -> UserMessageData { UserMessageData { id: format!("user-{content}"), diff --git a/src/crates/services/services-integrations/Cargo.toml b/src/crates/services/services-integrations/Cargo.toml index f9eb65a7d9..acf26d2264 100644 --- a/src/crates/services/services-integrations/Cargo.toml +++ b/src/crates/services/services-integrations/Cargo.toml @@ -355,6 +355,7 @@ remote-ssh-concrete = [ "anyhow", "async-trait", "base64", + "dep:openbitfun-core-types", "openbitfun-services-core/tls-provider", "dep:openbitfun-runtime-ports", "chrono", diff --git a/src/crates/services/services-integrations/src/remote_connect.rs b/src/crates/services/services-integrations/src/remote_connect.rs index 6128401ae5..693c8e52b4 100644 --- a/src/crates/services/services-integrations/src/remote_connect.rs +++ b/src/crates/services/services-integrations/src/remote_connect.rs @@ -1860,22 +1860,27 @@ pub struct RemoteModelCatalogPollDelta { } pub fn resolve_remote_agent_type(mobile_type: Option<&str>) -> &'static str { + if let Some(harness) = + mobile_type.and_then(openbitfun_core_types::agent_identity::HarnessId::from_legacy_id) + { + return harness.as_str(); + } match mobile_type { - Some(value) if value.eq_ignore_ascii_case("minimal") => "minimal", + Some(value) if value.eq_ignore_ascii_case("minimal") => "Minimal", Some(value) if value.eq_ignore_ascii_case("ultra") || value.eq_ignore_ascii_case("ultimate") => { - "Ultra" + "Ultimate" } Some(value) if value.eq_ignore_ascii_case("balanced") || value.eq_ignore_ascii_case("standard") => { - "agentic" + "Standard" } - Some("code") | Some("agentic") | Some("Agentic") => "agentic", + Some("code") => "Standard", Some("cowork") | Some("Cowork") => "Cowork", Some("claw") | Some("Claw") | Some("assistant") | Some("chat") => "Claw", - _ => "agentic", + _ => "Standard", } } @@ -1890,6 +1895,10 @@ pub struct ImageAttachment { pub struct SessionInfo { pub session_id: String, pub name: String, + #[serde( + serialize_with = "openbitfun_core_types::agent_identity_wire::serialize_legacy_agent_id", + deserialize_with = "openbitfun_core_types::agent_identity::deserialize_agent_id" + )] pub agent_type: String, pub created_at: String, pub updated_at: String, @@ -2751,7 +2760,7 @@ where ); info!( "Remote send_message: session={session_id}, agent_type={}, image_contexts={}", - agent_type.as_deref().unwrap_or("agentic"), + agent_type.as_deref().unwrap_or("Standard"), resolved_contexts.len() ); remote_dialog_submit_response( @@ -4106,7 +4115,7 @@ mod tests { RemoteSessionMetadata { session_id: "session-a".to_string(), name: "keep me".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), created_at_ms: 1_000, last_active_at_ms: 2_000, turn_count: 3, @@ -4114,7 +4123,7 @@ mod tests { RemoteSessionMetadata { session_id: "session-b".to_string(), name: "other".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), created_at_ms: 1_000, last_active_at_ms: 2_000, turn_count: 1, diff --git a/src/crates/services/services-integrations/src/remote_ssh/dispatch_ssh.rs b/src/crates/services/services-integrations/src/remote_ssh/dispatch_ssh.rs index 9a0a175c78..1929835d27 100644 --- a/src/crates/services/services-integrations/src/remote_ssh/dispatch_ssh.rs +++ b/src/crates/services/services-integrations/src/remote_ssh/dispatch_ssh.rs @@ -2039,6 +2039,12 @@ async fn invoke_json_at_path( verb: &'static str, request: &Value, ) -> Result { + use openbitfun_core_types::agent_identity_wire::{ + translate_agent_identity_command, translate_agent_identity_response, AgentIdentityDialect, + }; + let request = + translate_agent_identity_command(verb, request.clone(), AgentIdentityDialect::Legacy) + .map_err(anyhow::Error::msg)?; let request_dir = product_data_path(home, &["dispatch", "requests"]); exec_ok( manager, @@ -2052,7 +2058,7 @@ async fn invoke_json_at_path( ) .await?; let request_path = format!("{request_dir}/{}.json", uuid::Uuid::new_v4().as_simple()); - let request_bytes = serde_json::to_vec(request).context("serialize dispatch request")?; + let request_bytes = serde_json::to_vec(&request).context("serialize dispatch request")?; // Pre-create with 0600 before SFTP opens it. Creating first and chmodding // afterwards would leave a prompt briefly governed by the server's umask // (commonly 0644). @@ -2098,12 +2104,19 @@ async fn invoke_json_at_path( &result.stderr, )); } - serde_json::from_str(result.stdout.trim()).with_context(|| { + let response = serde_json::from_str(result.stdout.trim()).with_context(|| { format!( "dispatch {verb} returned invalid JSON: {}", bounded_detail(&result.stdout) ) - }) + })?; + translate_agent_identity_response( + verb, + response, + &Value::Null, + AgentIdentityDialect::Canonical, + ) + .map_err(anyhow::Error::msg) } fn dispatch_command(cli_path: &str, verb: &str, request_path: &str) -> String { diff --git a/src/crates/services/services-integrations/tests/remote_connect_contracts.rs b/src/crates/services/services-integrations/tests/remote_connect_contracts.rs index 43b4192d48..c85358be7f 100644 --- a/src/crates/services/services-integrations/tests/remote_connect_contracts.rs +++ b/src/crates/services/services-integrations/tests/remote_connect_contracts.rs @@ -1214,7 +1214,7 @@ async fn remote_connect_dialog_runtime_owns_restore_prewarm_and_submit_order() { let submitted = host.submitted(); assert_eq!(submitted.session_id, "session-1"); assert_eq!(submitted.content, "hello"); - assert_eq!(submitted.resolved_agent_type, "agentic"); + assert_eq!(submitted.resolved_agent_type, "Standard"); assert_eq!( submitted .binding_workspace @@ -1901,7 +1901,7 @@ fn remote_connect_session_response_helpers_own_pagination_and_timestamps() { RemoteSessionMetadata { session_id: "session-1".to_string(), name: "first".to_string(), - agent_type: "agentic".to_string(), + agent_type: "Standard".to_string(), created_at_ms: 1_700_000_000_000, last_active_at_ms: 1_700_000_001_000, turn_count: 3, @@ -2032,7 +2032,7 @@ fn remote_connect_session_response_helpers_own_pagination_and_timestamps() { fn remote_connect_session_create_contract_preserves_workspace_binding() { let request = build_remote_session_create_request( "Remote Session", - "agentic", + "Standard", Some("D:/workspace/project"), RemoteSessionWorkspaceIdentity::new( Some("ssh-1".to_string()), @@ -2042,7 +2042,7 @@ fn remote_connect_session_create_contract_preserves_workspace_binding() { ); assert_eq!(request.session_name, "Remote Session"); - assert_eq!(request.agent_type, "agentic"); + assert_eq!(request.agent_type, "Standard"); assert_eq!( request.workspace_path.as_deref(), Some("D:/workspace/project") @@ -2054,20 +2054,20 @@ fn remote_connect_session_create_contract_preserves_workspace_binding() { #[test] fn remote_connect_agent_type_mapping_preserves_current_mobile_aliases() { - assert_eq!(resolve_remote_agent_type(Some("code")), "agentic"); - assert_eq!(resolve_remote_agent_type(Some("agentic")), "agentic"); - assert_eq!(resolve_remote_agent_type(Some("Agentic")), "agentic"); - assert_eq!(resolve_remote_agent_type(Some("balanced")), "agentic"); - assert_eq!(resolve_remote_agent_type(Some("standard")), "agentic"); + assert_eq!(resolve_remote_agent_type(Some("code")), "Standard"); + assert_eq!(resolve_remote_agent_type(Some("Standard")), "Standard"); + assert_eq!(resolve_remote_agent_type(Some("Standard")), "Standard"); + assert_eq!(resolve_remote_agent_type(Some("balanced")), "Standard"); + assert_eq!(resolve_remote_agent_type(Some("standard")), "Standard"); assert_eq!(resolve_remote_agent_type(Some("minimal")), "minimal"); - assert_eq!(resolve_remote_agent_type(Some("ultimate")), "Ultra"); - assert_eq!(resolve_remote_agent_type(Some("Ultra")), "Ultra"); + assert_eq!(resolve_remote_agent_type(Some("ultimate")), "Ultimate"); + assert_eq!(resolve_remote_agent_type(Some("Ultimate")), "Ultimate"); assert_eq!(resolve_remote_agent_type(Some("cowork")), "Cowork"); assert_eq!(resolve_remote_agent_type(Some("Cowork")), "Cowork"); - assert_eq!(resolve_remote_agent_type(Some("plan")), "agentic"); - assert_eq!(resolve_remote_agent_type(Some("Plan")), "agentic"); - assert_eq!(resolve_remote_agent_type(Some("unknown")), "agentic"); - assert_eq!(resolve_remote_agent_type(None), "agentic"); + assert_eq!(resolve_remote_agent_type(Some("plan")), "Standard"); + assert_eq!(resolve_remote_agent_type(Some("Plan")), "Standard"); + assert_eq!(resolve_remote_agent_type(Some("unknown")), "Standard"); + assert_eq!(resolve_remote_agent_type(None), "Standard"); } #[test] diff --git a/src/mobile-web/src/App.tsx b/src/mobile-web/src/App.tsx index 11e5b6a857..7163588bf5 100644 --- a/src/mobile-web/src/App.tsx +++ b/src/mobile-web/src/App.tsx @@ -50,7 +50,7 @@ const AppContent: React.FC = () => { const [page, setPage] = useState('pairing'); const [activeSessionId, setActiveSessionId] = useState(null); const [activeSessionName, setActiveSessionName] = useState('Session'); - const [activeSessionAgentType, setActiveSessionAgentType] = useState('agentic'); + const [activeSessionAgentType, setActiveSessionAgentType] = useState('Standard'); const [chatAutoFocus, setChatAutoFocus] = useState(false); const [compactSidebarOpen, setCompactSidebarOpen] = useState(false); const isWideLayout = useWideLayout(); @@ -154,7 +154,7 @@ const AppContent: React.FC = () => { clearTimeout(timerRef.current); setActiveSessionId(null); setActiveSessionName('Session'); - setActiveSessionAgentType('agentic'); + setActiveSessionAgentType('Standard'); setChatAutoFocus(false); setPrevPage(null); setNavDir(null); @@ -229,7 +229,7 @@ const AppContent: React.FC = () => { sessionId: string, sessionName?: string, isNew?: boolean, - agentType = 'agentic', + agentType = 'Standard', ) => { setActiveSessionId(sessionId); setActiveSessionName(sessionName || 'Session'); @@ -275,7 +275,7 @@ const AppContent: React.FC = () => { } setActiveSessionId(null); setActiveSessionName('Session'); - setActiveSessionAgentType('agentic'); + setActiveSessionAgentType('Standard'); setChatAutoFocus(false); setPrevPage(null); setNavDir(null); @@ -298,7 +298,7 @@ const AppContent: React.FC = () => { setSessionMgr(null); setActiveSessionId(null); setActiveSessionName('Session'); - setActiveSessionAgentType('agentic'); + setActiveSessionAgentType('Standard'); setChatAutoFocus(false); setCompactSidebarOpen(false); setPrevPage(null); diff --git a/src/mobile-web/src/components/HarnessProfilePicker.tsx b/src/mobile-web/src/components/HarnessProfilePicker.tsx index 1fa2b2b37e..2e52b64adb 100644 --- a/src/mobile-web/src/components/HarnessProfilePicker.tsx +++ b/src/mobile-web/src/components/HarnessProfilePicker.tsx @@ -9,9 +9,9 @@ interface HarnessProfilePickerProps { } const PROFILES = [ - { agentType: 'minimal', labelKey: 'sessions.harnessMinimal', density: 1 }, - { agentType: 'agentic', labelKey: 'sessions.harnessStandard', density: 2 }, - { agentType: 'Ultra', labelKey: 'sessions.harnessUltimate', density: 3 }, + { agentType: 'Minimal', labelKey: 'sessions.harnessMinimal', density: 1 }, + { agentType: 'Standard', labelKey: 'sessions.harnessStandard', density: 2 }, + { agentType: 'Ultimate', labelKey: 'sessions.harnessUltimate', density: 3 }, ] as const; /** Creation-time Harness selector shared by the home and workspace entry points. */ diff --git a/src/mobile-web/src/components/SessionDashboardSections.tsx b/src/mobile-web/src/components/SessionDashboardSections.tsx index 861b0512f3..28c0e1327f 100644 --- a/src/mobile-web/src/components/SessionDashboardSections.tsx +++ b/src/mobile-web/src/components/SessionDashboardSections.tsx @@ -66,8 +66,8 @@ export const SessionLaunchPanel: React.FC = ({ {isProMode ? ( hasWorkspace ? (
- {action('code', t('shared.agents.code'), t('sessions.codeSessionDesc'), onRequestCodeHarness)} - {action('cowork', t('shared.agents.cowork'), t('sessions.coworkSessionDesc'), onCreateCowork)} + {action('code', t('shared.agents.Standard'), t('sessions.codeSessionDesc'), onRequestCodeHarness)} + {action('cowork', t('shared.agents.Cowork'), t('sessions.coworkSessionDesc'), onCreateCowork)}
) : null ) : ( diff --git a/src/mobile-web/src/i18n/generatedLocaleContract.ts b/src/mobile-web/src/i18n/generatedLocaleContract.ts index 91fdb13ddb..b1f42273d1 100644 --- a/src/mobile-web/src/i18n/generatedLocaleContract.ts +++ b/src/mobile-web/src/i18n/generatedLocaleContract.ts @@ -48,15 +48,19 @@ export const SHARED_TERMS_BY_LOCALE = { "workspace": "工作区" }, "modes": { - "agentic": "代理模式", "assistant": "助手模式", "expert": "专家模式", "review": "评审模式" }, "agents": { - "code": "代码会话", - "cowork": "协作会话", - "claw": "Claw", + "Minimal": "极简", + "Standard": "标准", + "Ultimate": "极致", + "Creative": "创造", + "Claw": "助理", + "Cowork": "办公协作", + "DeepResearch": "深度研究", + "ComputerUse": "电脑操作", "default": "默认助手" }, "tools": { @@ -97,15 +101,19 @@ export const SHARED_TERMS_BY_LOCALE = { "workspace": "工作區" }, "modes": { - "agentic": "代理模式", "assistant": "助手模式", "expert": "專家模式", "review": "審查模式" }, "agents": { - "code": "程式碼會話", - "cowork": "協作會話", - "claw": "Claw", + "Minimal": "極簡", + "Standard": "標準", + "Ultimate": "極致", + "Creative": "創造", + "Claw": "助理", + "Cowork": "辦公協作", + "DeepResearch": "深度研究", + "ComputerUse": "電腦操作", "default": "預設助手" }, "tools": { @@ -146,15 +154,19 @@ export const SHARED_TERMS_BY_LOCALE = { "workspace": "Workspace" }, "modes": { - "agentic": "Agentic Mode", "assistant": "Assistant Mode", "expert": "Expert Mode", "review": "Review Mode" }, "agents": { - "code": "Code Session", - "cowork": "Cowork Session", - "claw": "Claw", + "Minimal": "Minimal", + "Standard": "Standard", + "Ultimate": "Ultimate", + "Creative": "Creative", + "Claw": "Assistant", + "Cowork": "Cowork", + "DeepResearch": "Deep Research", + "ComputerUse": "Computer Use", "default": "Default Assistant" }, "tools": { diff --git a/src/mobile-web/src/i18n/messages.ts b/src/mobile-web/src/i18n/messages.ts index 4379a37371..69a70d2ab1 100644 --- a/src/mobile-web/src/i18n/messages.ts +++ b/src/mobile-web/src/i18n/messages.ts @@ -89,7 +89,7 @@ export const messages: Record = { selectWorkspace: 'Select Workspace', noWorkspaces: 'No workspaces found', noWorkspaceSelected: 'No workspace selected', - assistant: 'Assistant', + assistant: SHARED_TERMS_BY_LOCALE['en-US'].agents.Claw, switchAssistant: 'Switch assistant', selectAssistant: 'Select Assistant', launch: 'Launch', @@ -108,14 +108,14 @@ export const messages: Record = { remoteCoworkSession: 'Remote Cowork Session', remoteClawSession: 'Remote Claw Session', agentCode: 'Code', - agentCowork: 'Cowork', + agentCowork: SHARED_TERMS_BY_LOCALE['en-US'].agents.Cowork, agentDefault: 'Default', searchSessions: 'Search sessions...', sessionActions: 'Session actions', selectExecutionMode: 'Select execution mode', - harnessMinimal: 'Minimal', - harnessStandard: 'Standard', - harnessUltimate: 'Ultimate', + harnessMinimal: SHARED_TERMS_BY_LOCALE['en-US'].agents.Minimal, + harnessStandard: SHARED_TERMS_BY_LOCALE['en-US'].agents.Standard, + harnessUltimate: SHARED_TERMS_BY_LOCALE['en-US'].agents.Ultimate, renameSession: 'Rename', deleteSession: 'Delete', confirmDelete: 'Delete session?', @@ -358,7 +358,7 @@ export const messages: Record = { selectWorkspace: '选择工作区', noWorkspaces: '暂无工作区', noWorkspaceSelected: '未选择工作区', - assistant: '助理', + assistant: SHARED_TERMS_BY_LOCALE['zh-CN'].agents.Claw, switchAssistant: '切换助理', selectAssistant: '选择助理', launch: '启动', @@ -377,14 +377,14 @@ export const messages: Record = { remoteCoworkSession: '远程协作会话', remoteClawSession: '远程助理会话', agentCode: 'Code', - agentCowork: 'Cowork', + agentCowork: SHARED_TERMS_BY_LOCALE['zh-CN'].agents.Cowork, agentDefault: '默认', searchSessions: '搜索会话...', sessionActions: '会话操作', selectExecutionMode: '选择执行模式', - harnessMinimal: '极简', - harnessStandard: '标准', - harnessUltimate: '极致', + harnessMinimal: SHARED_TERMS_BY_LOCALE['zh-CN'].agents.Minimal, + harnessStandard: SHARED_TERMS_BY_LOCALE['zh-CN'].agents.Standard, + harnessUltimate: SHARED_TERMS_BY_LOCALE['zh-CN'].agents.Ultimate, renameSession: '重命名', deleteSession: '删除', confirmDelete: '确定删除此会话?', @@ -627,7 +627,7 @@ export const messages: Record = { selectWorkspace: '選擇工作區', noWorkspaces: '暫無工作區', noWorkspaceSelected: '未選擇工作區', - assistant: '助理', + assistant: SHARED_TERMS_BY_LOCALE['zh-TW'].agents.Claw, switchAssistant: '切換助理', selectAssistant: '選擇助理', launch: '啟動', @@ -646,14 +646,14 @@ export const messages: Record = { remoteCoworkSession: '遠程協作會話', remoteClawSession: '遠程助理會話', agentCode: 'Code', - agentCowork: 'Cowork', + agentCowork: SHARED_TERMS_BY_LOCALE['zh-TW'].agents.Cowork, agentDefault: '默認', searchSessions: '搜尋會話...', sessionActions: '會話操作', selectExecutionMode: '選擇執行模式', - harnessMinimal: '極簡', - harnessStandard: '標準', - harnessUltimate: '極致', + harnessMinimal: SHARED_TERMS_BY_LOCALE['zh-TW'].agents.Minimal, + harnessStandard: SHARED_TERMS_BY_LOCALE['zh-TW'].agents.Standard, + harnessUltimate: SHARED_TERMS_BY_LOCALE['zh-TW'].agents.Ultimate, renameSession: '重新命名', deleteSession: '刪除', confirmDelete: '確定刪除此會話?', diff --git a/src/mobile-web/src/pages/ChatPage.tsx b/src/mobile-web/src/pages/ChatPage.tsx index 300308fded..629c0ea053 100644 --- a/src/mobile-web/src/pages/ChatPage.tsx +++ b/src/mobile-web/src/pages/ChatPage.tsx @@ -65,7 +65,7 @@ const ChatPage: React.FC = ({ sessionMgr, sessionId, sessionName, - agentType: sessionAgentType = 'agentic', + agentType: sessionAgentType = 'Standard', onBack, autoFocus, wideLayout = false, diff --git a/src/mobile-web/src/pages/SessionListPage.tsx b/src/mobile-web/src/pages/SessionListPage.tsx index 4f8203262b..47512ff573 100644 --- a/src/mobile-web/src/pages/SessionListPage.tsx +++ b/src/mobile-web/src/pages/SessionListPage.tsx @@ -169,22 +169,20 @@ function formatTime( function agentLabel(agentType: string, t: (key: string) => string): string { switch (agentType) { - case 'minimal': + case 'Minimal': return t('sessions.harnessMinimal'); - case 'Ultra': - case 'ultra': - case 'ultimate': + case 'Ultimate': return t('sessions.harnessUltimate'); case 'code': return t('sessions.agentCode'); - case 'agentic': + case 'Standard': return t('sessions.harnessStandard'); case 'cowork': case 'Cowork': return t('sessions.agentCowork'); case 'claw': case 'Claw': - return t('shared.agents.claw'); + return t('shared.agents.Claw'); default: return agentType || t('sessions.agentDefault'); } diff --git a/src/mobile-web/src/services/MobileNavigationStore.ts b/src/mobile-web/src/services/MobileNavigationStore.ts index 30adb9d499..376e79d37b 100644 --- a/src/mobile-web/src/services/MobileNavigationStore.ts +++ b/src/mobile-web/src/services/MobileNavigationStore.ts @@ -1,3 +1,4 @@ +import { canonicalAgentId } from '../../../shared/agent-harness/contract.generated'; const STORAGE_KEY = 'openbitfun.mobile.navigation.v1'; export interface MobileNavigationScope { @@ -44,7 +45,7 @@ export function loadMobileNavigation( deviceId: record.deviceId, session: session && typeof session.id === 'string' && session.id.trim() && typeof session.name === 'string' && typeof session.agentType === 'string' - ? { id: session.id, name: session.name, agentType: session.agentType } + ? { id: session.id, name: session.name, agentType: canonicalAgentId(session.agentType) } : undefined, }; } catch { diff --git a/src/mobile-web/src/services/RemoteSessionManager.ts b/src/mobile-web/src/services/RemoteSessionManager.ts index ae65c0b38c..7093c73013 100644 --- a/src/mobile-web/src/services/RemoteSessionManager.ts +++ b/src/mobile-web/src/services/RemoteSessionManager.ts @@ -1,3 +1,4 @@ +import { translateAgentIdentityFields } from '../../../shared/agent-harness/wire'; /** * Manages remote sessions by sending commands to the desktop via the relay. * All communication is request-response via RelayHttpClient (HTTP). @@ -275,13 +276,17 @@ export class RemoteSessionManager { const targetDeviceId = target.deviceId; if (!targetDeviceId) throw new Error('Select an account device to continue'); try { - const resp = await this.client.sendDeviceRpc(targetDeviceId, cmdWithId, relayOptions); + const resp = await this.client.sendDeviceRpc( + targetDeviceId, + translateAgentIdentityFields(cmdWithId, 'legacy'), + relayOptions, + ); this.ensureControlTargetCurrent(target); const respAny = resp as any; if (respAny.resp === 'error') { throw new Error(respAny.message || 'Unknown error'); } - return resp; + return translateAgentIdentityFields(resp, 'canonical'); } catch (error: unknown) { // Suppress both successful and failed completions after a target switch. // The epoch check (rather than device id alone) also closes A -> B -> A diff --git a/src/mobile-web/src/styles/components/adaptive-shell.scss b/src/mobile-web/src/styles/components/adaptive-shell.scss index b70d291bfa..6d2492f5dc 100644 --- a/src/mobile-web/src/styles/components/adaptive-shell.scss +++ b/src/mobile-web/src/styles/components/adaptive-shell.scss @@ -382,7 +382,7 @@ color: var(--openbitfun-color-content-muted); &--code, - &--agentic, + &--Standard, &--cowork, &--Cowork, &--claw, diff --git a/src/mobile-web/src/styles/components/chat-input.scss b/src/mobile-web/src/styles/components/chat-input.scss index 45ac219d36..f52dc4bc5b 100644 --- a/src/mobile-web/src/styles/components/chat-input.scss +++ b/src/mobile-web/src/styles/components/chat-input.scss @@ -357,10 +357,6 @@ box-shadow: var(--openbitfun-shadow-sm); } - &:active:not(:disabled) { - transform: scale(0.92); - } - &:disabled { background: var(--openbitfun-color-action-neutral-surface); border-color: var(--openbitfun-color-border-subtle); @@ -380,11 +376,8 @@ &:hover:not(:disabled) { background: var(--openbitfun-color-action-primary-hover) !important; box-shadow: var(--openbitfun-shadow-base); - transform: scale(1.05); } - &:active { transform: scale(0.92); } - &:disabled { background: var(--openbitfun-color-action-primary-background) !important; color: var(--openbitfun-color-action-primary-content) !important; diff --git a/src/mobile-web/src/styles/components/chat.scss b/src/mobile-web/src/styles/components/chat.scss index 8dccd6cacc..0ce4823c24 100644 --- a/src/mobile-web/src/styles/components/chat.scss +++ b/src/mobile-web/src/styles/components/chat.scss @@ -197,7 +197,6 @@ animation: scrollBtnIn 200ms var(--easing-standard); &:active { - transform: translateX(-50%) scale(0.92); box-shadow: var(--openbitfun-shadow-sm); } diff --git a/src/mobile-web/src/styles/components/language-toggle.scss b/src/mobile-web/src/styles/components/language-toggle.scss index d22b124281..b42f72c3f1 100644 --- a/src/mobile-web/src/styles/components/language-toggle.scss +++ b/src/mobile-web/src/styles/components/language-toggle.scss @@ -16,7 +16,6 @@ transition: all var(--motion-fast) var(--easing-standard); &:active { - transform: scale(0.96); color: var(--openbitfun-color-content-primary); background: var(--openbitfun-color-action-neutral-surface-hover); } diff --git a/src/mobile-web/src/styles/components/sessions.scss b/src/mobile-web/src/styles/components/sessions.scss index 65767652bb..dd30a959d1 100644 --- a/src/mobile-web/src/styles/components/sessions.scss +++ b/src/mobile-web/src/styles/components/sessions.scss @@ -128,7 +128,6 @@ transition: all var(--motion-fast) var(--easing-standard); &:active { - transform: scale(0.96); color: var(--openbitfun-color-content-primary); background: var(--openbitfun-color-action-neutral-surface-hover); } @@ -212,7 +211,6 @@ animation: sessionItemIn var(--motion-base) motion.$easing-decelerate both; &:active { - transform: scale(0.985); background: var(--openbitfun-color-accent-surface); } @@ -729,7 +727,7 @@ flex-shrink: 0; &--code, - &--agentic { + &--Standard { background: var(--openbitfun-color-accent-surface); color: var(--openbitfun-color-accent-default); border-color: var(--openbitfun-color-accent-surface-strong); @@ -875,7 +873,6 @@ } &:active { - transform: scale(0.99); background: var(--openbitfun-color-surface-subtle); } diff --git a/src/shared/agent-harness/contract.generated.ts b/src/shared/agent-harness/contract.generated.ts new file mode 100644 index 0000000000..ed74911a01 --- /dev/null +++ b/src/shared/agent-harness/contract.generated.ts @@ -0,0 +1,46 @@ +// Generated by scripts/generate-agent-harness.mjs. Edit the shared contract instead. + +export const HARNESS_IDS = ["Minimal", "Standard", "Ultimate", "Creative"] as const; +export type HarnessId = typeof HARNESS_IDS[number]; +const aliases: ReadonlyMap = new Map([ + ["Minimal", "Minimal"], + ["minimal", "Minimal"], + ["Standard", "Standard"], + ["standard", "Standard"], + ["balanced", "Standard"], + ["agentic", "Standard"], + ["Agentic", "Standard"], + ["Ultimate", "Ultimate"], + ["ultimate", "Ultimate"], + ["Ultra", "Ultimate"], + ["ultra", "Ultimate"], + ["Creative", "Creative"], + ["creative", "Creative"], +]); +const legacyWireIds: Record = { + Minimal: "minimal", + Standard: "agentic", + Ultimate: "Ultra", + Creative: "Creative", +}; +const configAliases: ReadonlyMap = new Map([ + ["coding_shared", "Standard"], +]); +export function isHarnessId(id: string): id is HarnessId { + return (HARNESS_IDS as readonly string[]).includes(id); +} +/** Use only at a built-in identity boundary; source-qualified/custom IDs stay intact. */ +export function canonicalHarnessId(id: string | null | undefined): HarnessId | null { + const key = id?.trim(); + return key ? aliases.get(key) ?? aliases.get(key.toLowerCase()) ?? null : null; +} +export function canonicalAgentId(id: string): string { + return canonicalHarnessId(id) ?? id; +} +export function legacyAgentWireId(id: string): string { + const harness = canonicalHarnessId(id); + return harness ? legacyWireIds[harness] : id; +} +export function canonicalAgentConfigId(id: string): string { + return configAliases.get(id) ?? canonicalAgentId(id); +} diff --git a/src/shared/agent-harness/contract.json b/src/shared/agent-harness/contract.json new file mode 100644 index 0000000000..cc5e7b487d --- /dev/null +++ b/src/shared/agent-harness/contract.json @@ -0,0 +1,9 @@ +{ + "version": 1, + "harnesses": [ + { "id": "Minimal", "legacyIds": ["minimal"], "legacyWireId": "minimal" }, + { "id": "Standard", "legacyIds": ["standard", "balanced", "agentic", "Agentic"], "legacyConfigIds": ["coding_shared"], "legacyWireId": "agentic" }, + { "id": "Ultimate", "legacyIds": ["ultimate", "Ultra", "ultra"], "legacyWireId": "Ultra" }, + { "id": "Creative", "legacyIds": ["creative"], "legacyWireId": "Creative" } + ] +} diff --git a/src/shared/agent-harness/wire-fixtures.json b/src/shared/agent-harness/wire-fixtures.json new file mode 100644 index 0000000000..b2a83a788d --- /dev/null +++ b/src/shared/agent-harness/wire-fixtures.json @@ -0,0 +1,60 @@ +[ + { + "command": "submit", + "canonical": {"agentType": "Standard", "workspacePath": "/target/worktree", "prompt": "Keep agentic in this source text", "jobId": "job-1"}, + "legacy": {"agentType": "agentic", "workspacePath": "/target/worktree", "prompt": "Keep agentic in this source text", "jobId": "job-1"} + }, + { + "command": "dispatch_target_list", + "canonical": {}, + "legacy": {}, + "canonicalResponse": [{"agentType": "Ultimate", "sessionId": "session-1", "workspacePath": "/target/worktree"}], + "legacyResponse": [{"agentType": "Ultra", "sessionId": "session-1", "workspacePath": "/target/worktree"}] + }, + { + "command": "set_config", + "canonical": {"request": {"path": "app.flow_chat", "value": {"default_mode_strategy": "follow_last", "default_mode_id": "Standard", "last_mode_id": "Ultimate"}}}, + "legacy": {"request": {"path": "app.flow_chat", "value": {"default_mode_strategy": "follow_last", "default_mode_id": "agentic", "last_mode_id": "Ultra"}}} + }, + { + "command": "set_config", + "canonical": {"request": {"path": "app.flow_chat.last_mode_id", "value": "Minimal"}}, + "legacy": {"request": {"path": "app.flow_chat.last_mode_id", "value": "minimal"}} + }, + { + "command": "set_config", + "canonical": {"request": {"path": "ai.agent_profiles", "value": {"Standard": {"profile_id": "Standard", "future": {"agentType": "Standard"}}, "constructor": {"profile_id": "constructor"}, "plugin::agentic": {"profile_id": "plugin::agentic"}}}}, + "legacy": {"request": {"path": "ai.agent_profiles", "value": {"coding_shared": {"profile_id": "coding_shared", "future": {"agentType": "Standard"}}, "constructor": {"profile_id": "constructor"}, "plugin::agentic": {"profile_id": "plugin::agentic"}}}} + }, + { + "command": "set_config", + "canonical": {"request": {"path": "ai.agent_profiles.Standard", "value": {"profile_id": "Standard", "removed_tools": ["Bash"]}}}, + "legacy": {"request": {"path": "ai.agent_profiles.coding_shared", "value": {"profile_id": "coding_shared", "removed_tools": ["Bash"]}}} + }, + { + "command": "set_config", + "canonical": {"request": {"path": "custom.data", "value": {"agentType": "Standard", "profile_id": "Standard"}}}, + "legacy": {"request": {"path": "custom.data", "value": {"agentType": "Standard", "profile_id": "Standard"}}} + }, + { + "command": "get_config", + "canonical": {"request": {"path": "ai.agent_profiles.Standard"}}, + "legacy": {"request": {"path": "ai.agent_profiles.coding_shared"}}, + "canonicalResponse": {"profile_id": "Standard", "enabled_user_skills": ["user::docs"]}, + "legacyResponse": {"profile_id": "coding_shared", "enabled_user_skills": ["user::docs"]} + }, + { + "command": "get_config", + "canonical": {"request": {"path": "app.flow_chat.default_mode_id"}}, + "legacy": {"request": {"path": "app.flow_chat.default_mode_id"}}, + "canonicalResponse": "Standard", + "legacyResponse": "agentic" + }, + { + "command": "get_configs", + "canonical": {"request": {"paths": ["ai.agent_profiles.Standard", "app.flow_chat.last_mode_id", "custom.data"]}}, + "legacy": {"request": {"paths": ["ai.agent_profiles.coding_shared", "app.flow_chat.last_mode_id", "custom.data"]}}, + "canonicalResponse": {"ai.agent_profiles.Standard": {"profile_id": "Standard"}, "app.flow_chat.last_mode_id": "Ultimate", "custom.data": {"agentType": "Standard"}}, + "legacyResponse": {"ai.agent_profiles.coding_shared": {"profile_id": "coding_shared"}, "app.flow_chat.last_mode_id": "Ultra", "custom.data": {"agentType": "Standard"}} + } +] diff --git a/src/shared/agent-harness/wire.ts b/src/shared/agent-harness/wire.ts new file mode 100644 index 0000000000..35595d09f2 --- /dev/null +++ b/src/shared/agent-harness/wire.ts @@ -0,0 +1,119 @@ +/** The v0 peer/mobile dialect is isolated here; application state uses canonical IDs. */ +import { canonicalAgentId, canonicalAgentConfigId, legacyAgentWireId } from './contract.generated'; + +export type AgentIdentityDialect = 'canonical' | 'legacy'; +function identity(id: string, dialect: AgentIdentityDialect, profile = false): string { + const canonical = profile ? canonicalAgentConfigId(id) : canonicalAgentId(id); + return dialect === 'canonical' ? canonical + : profile && canonical === 'Standard' ? 'coding_shared' : legacyAgentWireId(canonical); +} +const agentFields = new Set([ + 'agentType', 'agent_type', 'parentAgentType', 'parent_agent_type', + 'modeId', 'mode_id', 'default_mode_id', 'last_mode_id', + 'lastUserDialogAgentType', 'last_user_dialog_agent_type', + 'lastSubmittedAgentType', 'last_submitted_agent_type', +]); +const opaque = new Set(['content', 'prompt', 'arguments', 'args', 'input', 'output', 'metadata', 'userMessageMetadata', 'user_message_metadata', 'value']); + +function isRecord(value: unknown): value is Record { + return Boolean(value && typeof value === 'object' && !Array.isArray(value)); +} + +function mergeProfileRecords(previous: unknown, incoming: unknown): unknown { + if (previous == null) return incoming; + if (incoming == null) return previous; + if (isRecord(previous) && isRecord(incoming)) { + const merged = new Map(Object.entries(previous)); + for (const [key, value] of Object.entries(incoming)) { + merged.set(key, merged.has(key) ? mergeProfileRecords(merged.get(key), value) : value); + } + return Object.fromEntries(merged); + } + if (JSON.stringify(previous) === JSON.stringify(incoming)) return previous; + throw new Error('Conflicting Agent profile aliases; original configuration was preserved'); +} + +function translateProfile(value: unknown, dialect: AgentIdentityDialect): unknown { + if (!isRecord(value) || typeof value.profile_id !== 'string') return value; + return { ...value, profile_id: identity(value.profile_id, dialect, true) }; +} + +function translateProfileMap(value: unknown, dialect: AgentIdentityDialect): unknown { + if (!isRecord(value)) return value; + const profiles = new Map(); + for (const [id, config] of Object.entries(value)) { + const key = identity(id, dialect, true); + const profile = translateProfile(config, dialect); + profiles.set(key, profiles.has(key) ? mergeProfileRecords(profiles.get(key), profile) : profile); + } + return Object.fromEntries(profiles); +} + +function configPath(path: string, dialect: AgentIdentityDialect): string { + const prefix = 'ai.agent_profiles.'; + if (!path.startsWith(prefix)) return path; + const [id, ...rest] = path.slice(prefix.length).split('.'); + return prefix + [identity(id, dialect, true), ...rest].join('.'); +} + +function configValue(path: string, value: unknown, dialect: AgentIdentityDialect): unknown { + if (path === 'ai.agent_profiles') return translateProfileMap(value, dialect); + if (['app.flow_chat.default_mode_id', 'app.flow_chat.last_mode_id'].includes(path)) { + return typeof value === 'string' ? identity(value, dialect) : value; + } + if (path.startsWith('ai.agent_profiles.')) { + const suffix = path.slice('ai.agent_profiles.'.length).split('.').slice(1).join('.'); + if (!suffix) return translateProfile(value, dialect); + return suffix === 'profile_id' && typeof value === 'string' ? identity(value, dialect, true) : value; + } + return ['', 'app', 'app.flow_chat', 'ai'].includes(path) + ? translateAgentIdentityFields(value, dialect) : value; +} +export function translateAgentIdentityFields(value: T, dialect: AgentIdentityDialect): T { + if (Array.isArray(value)) return value.map(item => translateAgentIdentityFields(item, dialect)) as T; + if (!value || typeof value !== 'object') return value; + const record = value as Record; + if (typeof record.source === 'string' && record.source !== 'builtin') return value; + const catalogEntry = 'toolCount' in record || 'defaultTools' in record; + return Object.fromEntries(Object.entries(record).map(([key, item]) => { + const profile = key === 'configProfileId' || key === 'profile_id'; + if ((key === 'agent_profiles' || key === 'ai.agent_profiles') && item && typeof item === 'object' && !Array.isArray(item)) { + return [key, translateProfileMap(item, dialect)]; + } + if (typeof item === 'string' && (profile || agentFields.has(key) || (catalogEntry && key === 'id'))) { + return [key, identity(item, dialect, profile)]; + } + if (['configProfileMemberModeIds', 'allowedParentAgentIds', 'deniedParentAgentIds'].includes(key) && Array.isArray(item)) { + return [key, item.map(id => typeof id === 'string' ? identity(id, dialect) : id)]; + } + return [key, opaque.has(key) ? item : translateAgentIdentityFields(item, dialect)]; + })) as T; +} +export function translateAgentIdentityCommand(command: string, value: T, dialect: AgentIdentityDialect): T { + const translated = translateAgentIdentityFields(value, dialect); + if (['set_config', 'get_config', 'get_configs'].includes(command) && isRecord(translated)) { + const request = isRecord(translated.request) ? translated.request : translated; + if (typeof request.path === 'string') { + if (command === 'set_config') request.value = configValue(request.path, request.value, dialect); + request.path = configPath(request.path, dialect); + } + if (command === 'get_configs' && Array.isArray(request.paths)) { + request.paths = request.paths.map(path => typeof path === 'string' ? configPath(path, dialect) : path); + } + } + return translated; +} + +/** Config replies need the request scope because a path read may return a scalar or map. */ +export function translateAgentIdentityResponse(command: string, value: T, args: unknown, dialect: AgentIdentityDialect): T { + if (command === 'get_agent_profile_configs') return translateProfileMap(value, dialect) as T; + if (command === 'get_config') { + const request = isRecord(args) && isRecord(args.request) ? args.request : args; + const path = isRecord(request) && typeof request.path === 'string' ? request.path : ''; + return configValue(path, value, dialect) as T; + } + if (command === 'get_configs' && isRecord(value)) { + return Object.fromEntries(Object.entries(value).map(([path, config]) => [configPath(path, dialect), configValue(path, config, dialect)])) as T; + } + return translateAgentIdentityFields(value, dialect); +} diff --git a/src/shared/i18n/resources/shared/en-US/terms.json b/src/shared/i18n/resources/shared/en-US/terms.json index c97bb65b12..c4739f2a5d 100644 --- a/src/shared/i18n/resources/shared/en-US/terms.json +++ b/src/shared/i18n/resources/shared/en-US/terms.json @@ -11,15 +11,19 @@ "workspace": "Workspace" }, "modes": { - "agentic": "Agentic Mode", "assistant": "Assistant Mode", "expert": "Expert Mode", "review": "Review Mode" }, "agents": { - "code": "Code Session", - "cowork": "Cowork Session", - "claw": "Claw", + "Minimal": "Minimal", + "Standard": "Standard", + "Ultimate": "Ultimate", + "Creative": "Creative", + "Claw": "Assistant", + "Cowork": "Cowork", + "DeepResearch": "Deep Research", + "ComputerUse": "Computer Use", "default": "Default Assistant" }, "tools": { diff --git a/src/shared/i18n/resources/shared/zh-CN/terms.json b/src/shared/i18n/resources/shared/zh-CN/terms.json index 90eadddded..b7a5bec5d2 100644 --- a/src/shared/i18n/resources/shared/zh-CN/terms.json +++ b/src/shared/i18n/resources/shared/zh-CN/terms.json @@ -11,15 +11,19 @@ "workspace": "工作区" }, "modes": { - "agentic": "代理模式", "assistant": "助手模式", "expert": "专家模式", "review": "评审模式" }, "agents": { - "code": "代码会话", - "cowork": "协作会话", - "claw": "Claw", + "Minimal": "极简", + "Standard": "标准", + "Ultimate": "极致", + "Creative": "创造", + "Claw": "助理", + "Cowork": "办公协作", + "DeepResearch": "深度研究", + "ComputerUse": "电脑操作", "default": "默认助手" }, "tools": { diff --git a/src/shared/i18n/resources/shared/zh-TW/terms.json b/src/shared/i18n/resources/shared/zh-TW/terms.json index 9d648486b2..5c2fbc02d2 100644 --- a/src/shared/i18n/resources/shared/zh-TW/terms.json +++ b/src/shared/i18n/resources/shared/zh-TW/terms.json @@ -11,15 +11,19 @@ "workspace": "工作區" }, "modes": { - "agentic": "代理模式", "assistant": "助手模式", "expert": "專家模式", "review": "審查模式" }, "agents": { - "code": "程式碼會話", - "cowork": "協作會話", - "claw": "Claw", + "Minimal": "極簡", + "Standard": "標準", + "Ultimate": "極致", + "Creative": "創造", + "Claw": "助理", + "Cowork": "辦公協作", + "DeepResearch": "深度研究", + "ComputerUse": "電腦操作", "default": "預設助手" }, "tools": { diff --git a/src/web-ui/src/app/components/GalleryLayout/GalleryDetailModal.presentation.test.ts b/src/web-ui/src/app/components/GalleryLayout/GalleryDetailModal.presentation.test.ts index 6aad2c68f9..3c81085e4c 100644 --- a/src/web-ui/src/app/components/GalleryLayout/GalleryDetailModal.presentation.test.ts +++ b/src/web-ui/src/app/components/GalleryLayout/GalleryDetailModal.presentation.test.ts @@ -46,11 +46,8 @@ describe('GalleryDetailModal presentation contract', () => { expect(agentCardStyles).toContain('@container gallery-detail-modal (max-width: 460px)'); expect(agentCardStyles).toContain('grid-template-columns: 150px minmax(0, 1fr);'); expect(agentCardStyles).toMatch(/&__config-main\s*\{[\s\S]*?height:\s*100%;/); - expect(agentCardStyles).toMatch(/> \.agent-card__config-panel,[\s\S]*?min-height:\s*100%;/); - expect(agentCardStyles).toContain('grid-template-rows: repeat(2, minmax(52px, auto)) minmax(76px, 1fr);'); expect(agentCardStyles).not.toContain('&__detail-view-tabs'); expect(agentCardStyles).not.toContain('&__overview'); - expect(agentCardStyles).toMatch(/&__config-nav-item\s*\{[\s\S]*?border:\s*0;/); expect(agentCardStyles).not.toContain('box-shadow: inset 2px 0 0'); expect(agentCardStyles).not.toContain('box-shadow: inset 0 -2px 0'); }); diff --git a/src/web-ui/src/app/components/GalleryLayout/GalleryDetailModal.tsx b/src/web-ui/src/app/components/GalleryLayout/GalleryDetailModal.tsx index 32f8a2f355..6636f4fdf3 100644 --- a/src/web-ui/src/app/components/GalleryLayout/GalleryDetailModal.tsx +++ b/src/web-ui/src/app/components/GalleryLayout/GalleryDetailModal.tsx @@ -13,6 +13,7 @@ import './GalleryDetailModal.scss'; interface GalleryDetailModalProps { isOpen: boolean; onClose: () => void; + className?: string; icon?: React.ReactNode; iconGradient?: string; title: string; @@ -34,6 +35,7 @@ interface GalleryDetailModalProps { const GalleryDetailModal: React.FC = ({ isOpen, onClose, + className, icon, iconGradient, title, @@ -92,7 +94,10 @@ const GalleryDetailModal: React.FC = ({ onOpenChange={(nextOpen) => { if (!nextOpen) onClose(); }} size={size} aria-labelledby={usesHeroTitle ? heroTitleId : undefined} - className={stableHeight ? 'gallery-detail-modal__surface--stable-height' : undefined} + className={[ + stableHeight ? 'gallery-detail-modal__surface--stable-height' : '', + className, + ].filter(Boolean).join(' ') || undefined} data-testid={testId} > diff --git a/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss b/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss index b45c265346..6bca14e38d 100644 --- a/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss +++ b/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss @@ -266,10 +266,6 @@ $content-max: 936px; cursor: not-allowed; } - &:active:not(:disabled) { - transform: scale(0.975); - } - &:focus-visible { outline: 2px solid var(--openbitfun-color-accent-default); outline-offset: -1px; @@ -342,10 +338,6 @@ $content-max: 936px; outline: 2px solid var(--openbitfun-color-accent-default); outline-offset: -1px; } - - &:active { - transform: scale(0.975); - } } .gallery-filter-chip, diff --git a/src/web-ui/src/app/components/NavBar/NavBar.scss b/src/web-ui/src/app/components/NavBar/NavBar.scss index 09b3c8dee4..9adcd4af95 100644 --- a/src/web-ui/src/app/components/NavBar/NavBar.scss +++ b/src/web-ui/src/app/components/NavBar/NavBar.scss @@ -77,7 +77,6 @@ } &:active:not(.is-inactive) { - transform: scale(0.96); background: var(--openbitfun-color-action-neutral-surface-hover); } diff --git a/src/web-ui/src/app/components/NavPanel/NavPanel.scss b/src/web-ui/src/app/components/NavPanel/NavPanel.scss index 07a679e579..8141a2216e 100644 --- a/src/web-ui/src/app/components/NavPanel/NavPanel.scss +++ b/src/web-ui/src/app/components/NavPanel/NavPanel.scss @@ -801,7 +801,6 @@ $_section-header-height: 22px; } &:hover { - transform: translateY(-2px) scale(1.06); box-shadow: 0 10px 20px color-mix(in srgb, var(--openbitfun-color-content-on-light) 24%, transparent); filter: saturate(1); opacity: 1; @@ -2110,11 +2109,6 @@ $_section-header-height: 22px; transition: none; } - .openbitfun-nav-panel__top-action-btn:hover .openbitfun-nav-panel__top-action-icon-circle, - .openbitfun-nav-panel__top-action-btn:active .openbitfun-nav-panel__top-action-icon-circle { - transform: scale(1); - } - .openbitfun-nav-panel__top-action-btn:not(.openbitfun-nav-panel__top-action-btn--sub) .openbitfun-nav-panel__top-action-icon-slot { transition: none; } @@ -2351,8 +2345,6 @@ $_section-header-height: 22px; background: var(--openbitfun-color-action-primary-background); color: var(--openbitfun-color-action-primary-content); box-shadow: var(--openbitfun-shadow-sm); - transform: scale(1); - transform-origin: center; .openbitfun-nav-panel__top-action-btn:hover & { background: var(--openbitfun-color-action-primary-hover); @@ -2365,7 +2357,6 @@ $_section-header-height: 22px; background: var(--openbitfun-color-action-primary-pressed); color: var(--openbitfun-color-action-primary-content); box-shadow: var(--openbitfun-shadow-xs); - transform: scale(0.86); } } @@ -2472,7 +2463,6 @@ $_section-header-height: 22px; color 120ms ease; &:active { - transform: scale(0.985); background: var(--openbitfun-color-action-neutral-surface-hover); } @@ -2498,10 +2488,6 @@ $_section-header-height: 22px; .openbitfun-nav-panel__top-action-btn:hover & { transform: none; } - - .openbitfun-nav-panel__top-action-btn:active & { - transform: scale(0.95); - } } &__sections { @@ -2536,10 +2522,6 @@ $_section-header-height: 22px; transition: transform 120ms cubic-bezier(0.23, 1, 0.32, 1), background 120ms ease; - - &--interactive:active { - transform: scale(0.985); - } } &__section-label { @@ -2555,10 +2537,6 @@ $_section-header-height: 22px; transform 120ms cubic-bezier(0.23, 1, 0.32, 1), color 120ms ease, background 120ms ease; - - &:active { - transform: scale(0.94); - } } &__item-slot { @@ -2588,10 +2566,6 @@ $_section-header-height: 22px; color 120ms ease, opacity 120ms ease; - &:active { - transform: scale(0.985); - } - &.is-active { @include nav-font.nav-panel-text-row-active; @include shell-surfaces.navigation-selection-feedback; @@ -2639,10 +2613,6 @@ $_section-header-height: 22px; &.is-active { box-shadow: none; } - - &:active { - transform: scale(0.985); - } } &__pages-entry.is-active, @@ -2662,13 +2632,8 @@ $_section-header-height: 22px; filter 120ms ease; &:hover { - transform: scale(1.05); box-shadow: none; } - - &:active { - transform: scale(0.94); - } } &__mode-pill { @@ -2683,10 +2648,6 @@ $_section-header-height: 22px; transform: none; box-shadow: none; } - - &:active { - transform: scale(0.985); - } } &__mode-pill-inner, @@ -2720,11 +2681,6 @@ $_section-header-height: 22px; color 120ms ease; } -.openbitfun-nav-panel__footer-btn--icon:active, -.openbitfun-nav-panel__footer .openbitfun-notification-btn:active { - transform: scale(0.94); -} - .openbitfun-nav-panel__workspace-menu, .openbitfun-nav-panel__footer-menu, .openbitfun-nav-panel__appearance-submenu { @@ -2742,10 +2698,6 @@ $_section-header-height: 22px; border-color 120ms ease, background 120ms ease, color 120ms ease; - - &:active:not(:disabled) { - transform: scale(0.97); - } } @keyframes openbitfun-nav-popover-in { diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss index a3eb0371ad..609218e64b 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss @@ -737,12 +737,10 @@ padding: 0 6px 0 var(--openbitfun-nav-session-rail); border-radius: var(--openbitfun-radius-sm); transition: - transform 120ms cubic-bezier(0.23, 1, 0.32, 1), background 120ms ease, color 120ms ease; &:active:not(.is-disabled) { - transform: scale(0.99); background: var(--openbitfun-color-action-neutral-surface-hover); } @@ -756,13 +754,8 @@ &__inline-item-edit-btn { border-radius: var(--openbitfun-radius-sm); transition: - transform 120ms cubic-bezier(0.23, 1, 0.32, 1), background 120ms ease, color 120ms ease; - - &:active { - transform: scale(0.94); - } } // Press feedback matches a full-width row, not the 20px icon buttons: the @@ -770,12 +763,10 @@ &__inline-toggle { border-radius: var(--openbitfun-radius-sm); transition: - transform 120ms cubic-bezier(0.23, 1, 0.32, 1), background 120ms ease, color 120ms ease; &:active:not(:disabled) { - transform: scale(0.99); background: var(--openbitfun-color-action-neutral-surface-hover); } } diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx index bbb3aee964..af38ca7295 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx @@ -681,7 +681,7 @@ const WorkspaceItem: React.FC = ({ } }, [t, workspace.rootPath]); - const handleCreateSession = useCallback(async (mode?: 'agentic' | 'Cowork' | 'Claw') => { + const handleCreateSession = useCallback(async (mode?: 'Standard' | 'Cowork' | 'Claw') => { setMenuOpen(false); const resolvedMode = mode ?? (workspace.workspaceKind === WorkspaceKind.Assistant ? 'Claw' : undefined); try { diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss index eb8e0729e0..e808331bfb 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss +++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss @@ -1324,13 +1324,8 @@ &__workspace-group-title-action { border-radius: var(--openbitfun-radius-sm); transition: - transform 120ms cubic-bezier(0.23, 1, 0.32, 1), background 120ms ease, color 120ms ease; - - &:active { - transform: scale(0.92); - } } &__workspace-item, @@ -1350,7 +1345,6 @@ background: transparent; box-shadow: none; transition: - transform 120ms cubic-bezier(0.23, 1, 0.32, 1), background 120ms ease, color 120ms ease; } @@ -1368,11 +1362,6 @@ background: transparent; } - &__workspace-item-card:active, - &__assistant-item-card:active { - transform: scale(0.99); - } - &__workspace-item-label, &__assistant-item-label { font-weight: var(--openbitfun-type-body-sm-font-weight); @@ -1383,28 +1372,12 @@ font-weight: var(--openbitfun-type-label-sm-font-weight); } - &__workspace-item-collapse-btn, - &__assistant-item-collapse-btn, - &__workspace-item-name-btn, - &__assistant-item-name-btn { - transition: transform 120ms cubic-bezier(0.23, 1, 0.32, 1); - - &:active { - transform: scale(0.97); - } - } - &__workspace-item-menu-trigger, &__assistant-item-menu-trigger { border-radius: var(--openbitfun-radius-sm); transition: - transform 120ms cubic-bezier(0.23, 1, 0.32, 1), background 120ms ease, color 120ms ease; - - &:active { - transform: scale(0.92); - } } &__workspace-item-sessions, diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.scss b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.scss index 6734512be0..bfd6013656 100644 --- a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.scss +++ b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.scss @@ -494,10 +494,6 @@ ); } - &:active { - transform: scale(0.98); - } - &:focus-visible { outline: var(--openbitfun-focus-width) solid var(--openbitfun-color-focus-ring); outline-offset: 3px; diff --git a/src/web-ui/src/app/components/SceneBar/SceneBar.scss b/src/web-ui/src/app/components/SceneBar/SceneBar.scss index 6eb04f304d..7941484915 100644 --- a/src/web-ui/src/app/components/SceneBar/SceneBar.scss +++ b/src/web-ui/src/app/components/SceneBar/SceneBar.scss @@ -131,7 +131,6 @@ } &:active { - transform: scale(0.94); background: var(--openbitfun-color-action-neutral-surface-pressed); } diff --git a/src/web-ui/src/app/components/TitleBar/NotificationButton.scss b/src/web-ui/src/app/components/TitleBar/NotificationButton.scss index 1dfcd8a4ad..d38b29dc45 100644 --- a/src/web-ui/src/app/components/TitleBar/NotificationButton.scss +++ b/src/web-ui/src/app/components/TitleBar/NotificationButton.scss @@ -25,7 +25,6 @@ } &:active { - transform: scale(0.94); background: var(--openbitfun-color-action-neutral-surface-hover); } diff --git a/src/web-ui/src/app/components/panels/TerminalEditModal.scss b/src/web-ui/src/app/components/panels/TerminalEditModal.scss index e0084b143c..7170323d2e 100644 --- a/src/web-ui/src/app/components/panels/TerminalEditModal.scss +++ b/src/web-ui/src/app/components/panels/TerminalEditModal.scss @@ -52,10 +52,6 @@ &:hover { background: var(--openbitfun-color-action-quiet-hover); } - - &:active { - transform: scale(0.97); - } } &--confirm { @@ -66,10 +62,6 @@ background: var(--openbitfun-color-accent-hover); } - &:active:not(:disabled) { - transform: scale(0.97); - } - &:disabled { opacity: 0.5; cursor: not-allowed; diff --git a/src/web-ui/src/app/components/panels/content-canvas/ContentCanvas.tsx b/src/web-ui/src/app/components/panels/content-canvas/ContentCanvas.tsx index bc96660e3a..0c4f3d6a56 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/ContentCanvas.tsx +++ b/src/web-ui/src/app/components/panels/content-canvas/ContentCanvas.tsx @@ -1,6 +1,6 @@ /** * ContentCanvas main container component. - * Core component for the right panel, aggregating submodules. + * Shared content surface. The containing scene or panel owns its layout. */ import React, { useCallback, useEffect, useMemo, useRef } from 'react'; @@ -9,9 +9,9 @@ import { AnchorZone } from './anchor-zone'; import { MissionControl } from './mission-control'; import { EmptyState } from './empty-state'; import { useCanvasStore } from './stores'; -import { useTabLifecycle, useKeyboardShortcuts, usePanelTabCoordinator } from './hooks'; +import { useTabLifecycle, useKeyboardShortcuts } from './hooks'; import type { AnchorPosition } from './types'; -import { TAB_EVENTS } from './types'; +import type { CanvasStoreMode } from './stores/canvasStore'; import { selectActiveBtwSessionTab } from '@/flow_chat/services/btwSessionPane'; import { openMainSession } from '@/flow_chat/services/sessionActivation'; import { isSamePath } from '@/shared/utils/pathUtils'; @@ -20,7 +20,7 @@ export interface ContentCanvasProps { /** Workspace path */ workspacePath?: string; /** App mode */ - mode?: 'agent' | 'project' | 'git' | 'bottom-terminal'; + mode?: CanvasStoreMode; /** Whether the containing scene is currently visible */ isSceneActive?: boolean; /** Interaction callback */ @@ -31,13 +31,9 @@ export interface ContentCanvasProps { disablePopOut?: boolean; /** Override the event this canvas listens to for creating tabs. */ createTabEventName?: string; - /** Override the expansion event this canvas dispatches/listens for. */ - expandPanelEventName?: string; - /** Custom collapsed state for canvases hosted outside the right panel. */ - isPanelCollapsed?: boolean; - /** Custom expand behavior for canvases hosted outside the right panel. */ - onExpandPanel?: () => void; - /** Custom collapse behavior for canvases hosted outside the right panel. */ + /** Reveal this host after an explicit content-open request. */ + onReveal?: () => void; + /** Close the containing panel, when this host is collapsible. */ onCollapsePanel?: () => void; /** Suspend terminal fit/PTY resize while the hosting panel is animating. */ terminalResizeSuspended?: boolean; @@ -54,9 +50,7 @@ export const ContentCanvas: React.FC = ({ onInteraction, disablePopOut = false, createTabEventName, - expandPanelEventName = TAB_EVENTS.EXPAND_RIGHT_PANEL, - isPanelCollapsed, - onExpandPanel, + onReveal, onCollapsePanel, terminalResizeSuspended = false, missionControlEnabled = true, @@ -82,21 +76,13 @@ export const ContentCanvas: React.FC = ({ const { handleCloseWithDirtyCheck, handleCloseAllWithDirtyCheck } = useTabLifecycle({ mode, createTabEventName, - expandPanelEventName, + onReveal, }); useKeyboardShortcuts({ enabled: true, missionControlEnabled, handleCloseWithDirtyCheck, - }); - // Panel/tab state coordinator (auto manage expand/collapse) - const { collapsePanel } = usePanelTabCoordinator({ - autoCollapseOnEmpty: true, - autoExpandOnTabOpen: true, - isCollapsed: isPanelCollapsed, - onExpand: onExpandPanel, - onCollapse: onCollapsePanel, - expandEventName: expandPanelEventName, + onReveal, }); useEffect(() => { @@ -160,7 +146,7 @@ export const ContentCanvas: React.FC = ({ // Show empty state when there are no visible tabs and no terminal keep-alive tabs. if (!hasRenderableTabs) { return ( - + {emptyState} ); diff --git a/src/web-ui/src/app/components/panels/content-canvas/hooks/README.md b/src/web-ui/src/app/components/panels/content-canvas/hooks/README.md index 9574c6a265..f53c3a6181 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/hooks/README.md +++ b/src/web-ui/src/app/components/panels/content-canvas/hooks/README.md @@ -2,27 +2,39 @@ ## usePanelTabCoordinator -Tab and panel state coordinator that manages synchronized state changes. +Installed by collapsible hosts (`AuxPane` and `BottomTerminalPane`), not by +`ContentCanvas`. Each host provides its own explicit expand/collapse actions +and expansion event. File Viewer, Git and Panel View own independent content +and never inherit the session's panel controls. ### Features -1. **Auto-collapse**: collapse the right panel when all tabs are closed -2. **Auto-expand**: expand the right panel when a tab opens -3. **Event listening**: listen to `expand-right-panel` to ensure all open paths expand the panel -4. **State sync**: keep tab and panel state consistent +1. Reveal only for explicit content-open requests or the host's expansion event. +2. Preserve manual collapse on mount, content updates and workspace restoration. +3. Collapse on a nonempty-to-empty transition within the same scope; count all + three editor groups. An explicitly opened empty pane stays open. +4. Layout actions set the intended state synchronously and idempotently, without + delayed toggles that can outlive the host. ### Usage ```tsx -usePanelTabCoordinator({ - autoCollapseOnEmpty: true, // Auto-collapse when all tabs are closed - autoExpandOnTabOpen: true, // Auto-expand when a tab opens +const { expandPanel, collapsePanel } = usePanelTabCoordinator({ + visibleTabCount, + scopeKey: canvasWorkspaceKey, + expandEventName: TAB_EVENTS.EXPAND_RIGHT_PANEL, + onExpand: expandSessionAuxPane, + onCollapse: collapseSessionAuxPane, }); + +// Pass onReveal={expandPanel} and onCollapsePanel={collapsePanel} to ContentCanvas. ``` -### Design principles +### Verification + +```bash +pnpm --dir src/web-ui run test:run src/app/components/panels/content-canvas/hooks/canvasPanelOwnership.test.tsx +``` -1. **Single responsibility**: coordinate state only, do not replace store methods -2. **Reactive**: listen to tab count changes and respond automatically -3. **Debounced**: use `requestAnimationFrame` to avoid frequent updates -4. **Event-driven**: coordinate with other modules through events +This exercises real stores, file-open events, lifecycle hooks and the layout +owner. It verifies state behavior, not visual appearance or remote transport. diff --git a/src/web-ui/src/app/components/panels/content-canvas/hooks/canvasPanelOwnership.test.tsx b/src/web-ui/src/app/components/panels/content-canvas/hooks/canvasPanelOwnership.test.tsx new file mode 100644 index 0000000000..24fd7aec01 --- /dev/null +++ b/src/web-ui/src/app/components/panels/content-canvas/hooks/canvasPanelOwnership.test.tsx @@ -0,0 +1,258 @@ +// @vitest-environment jsdom +import React, { act } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { + CanvasStoreModeContext, + clearAgentCanvasForPeerSwitch, + switchAgentCanvasWorkspace, + useAgentCanvasStore, + useBottomTerminalCanvasStore, + useCanvasStore, + useGitCanvasStore, + usePanelViewCanvasStore, + useProjectCanvasStore, +} from '../stores'; +import type { CanvasStoreMode } from '../stores/canvasStore'; +import { TAB_EVENTS, type PanelContent } from '../types'; +import { appManager } from '@/app/services/AppManager'; +import { + collapseSessionAuxPane, + collapseSessionBottomTerminalPane, + expandSessionAuxPane, + expandSessionBottomTerminalPane, +} from '@/app/scenes/session/sessionPanelLayout'; +import { fileTabManager } from '@/shared/services/FileTabManager'; +import { drainPendingTabs } from '@/shared/services/pendingTabQueue'; +import { usePanelTabCoordinator } from './usePanelTabCoordinator'; +import { useTabLifecycle } from './useTabLifecycle'; + +// Real stores, layout owner, file producer and lifecycle hooks. These probes +// exercise state transitions without replacing adapters or rendering editors. +function CanvasProbe({ mode, onReveal }: { mode: CanvasStoreMode; onReveal?: () => void }) { + useTabLifecycle({ mode, onReveal }); + return null; +} + +const expandBottom = () => expandSessionBottomTerminalPane(240); + +function PanelProbe({ bottom = false }: { bottom?: boolean }) { + const scopeKey = useCanvasStore(state => state.workspaceKey); + const visibleTabCount = useCanvasStore(state => ( + [state.primaryGroup, state.secondaryGroup, state.tertiaryGroup] + .reduce((count, group) => count + group.tabs.filter(tab => !tab.isHidden).length, 0) + )); + const { expandPanel } = usePanelTabCoordinator({ + visibleTabCount, + scopeKey, + expandEventName: bottom ? TAB_EVENTS.EXPAND_BOTTOM_TERMINAL_PANEL : TAB_EVENTS.EXPAND_RIGHT_PANEL, + onExpand: bottom ? expandBottom : expandSessionAuxPane, + onCollapse: bottom ? collapseSessionBottomTerminalPane : collapseSessionAuxPane, + }); + return ; +} + +function Hosts({ project = true }: { project?: boolean }) { + return <> + + + + + + + {project && + + } + + + + + + + ; +} + +const content = (title: string): PanelContent => ({ type: 'text-viewer', title, data: { content: title } }); + +describe('canvas host panel ownership', () => { + let root: Root; + let container: HTMLDivElement; + const stores = [useAgentCanvasStore, useProjectCanvasStore, useGitCanvasStore, + usePanelViewCanvasStore, useBottomTerminalCanvasStore]; + + beforeEach(() => { + (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + clearAgentCanvasForPeerSwitch(); + stores.forEach(store => store.getState().reset()); + ['agent', 'project', 'git'].forEach(mode => drainPendingTabs(mode as 'agent' | 'project' | 'git')); + appManager.updateLayout({ + chatCollapsed: false, + rightPanelCollapsed: true, + rightPanelWidth: 520, + bottomTerminalPanelCollapsed: true, + }); + container = document.createElement('div'); + root = createRoot(container); + }); + + afterEach(async () => { + await act(async () => root.unmount()); + clearAgentCanvasForPeerSwitch(); + stores.forEach(store => store.getState().reset()); + container.remove(); + }); + + it.each([true, false])('preserves a session panel with collapsed=%s through file-view lifetime', async collapsed => { + appManager.updateLayout({ rightPanelCollapsed: collapsed }); + useAgentCanvasStore.getState().addTab(content('session'), 'active'); + useProjectCanvasStore.getState().addTab(content('restored-file'), 'active'); + const before = appManager.getState().layout; + await act(async () => root.render()); + expect(appManager.getState().layout).toEqual(before); + + let rightPanelRequests = 0; + const onRightPanelRequest = () => { rightPanelRequests++; }; + window.addEventListener(TAB_EVENTS.EXPAND_RIGHT_PANEL, onRightPanelRequest); + try { + await act(async () => { + const options = { filePath: '/workspace/example.ts', workspacePath: '/workspace', + remoteConnectionId: 'ssh-workspace', mode: 'project' as const }; + fileTabManager.openFile(options); + fileTabManager.openFile({ ...options, jumpToLine: 12 }); + }); + const tabs = useProjectCanvasStore.getState().primaryGroup.tabs; + expect(tabs).toHaveLength(2); + expect(tabs.find(tab => tab.content.data?.filePath === '/workspace/example.ts')?.content.data).toMatchObject({ + filePath: '/workspace/example.ts', remoteConnectionId: 'ssh-workspace', jumpToRange: { start: 12 }, + }); + expect(rightPanelRequests).toBe(0); + await act(async () => useProjectCanvasStore.getState().closeAllTabs()); + await act(async () => root.render()); + await act(async () => { await new Promise(resolve => requestAnimationFrame(() => resolve())); }); + expect(appManager.getState().layout).toEqual(before); + expect(useAgentCanvasStore.getState().primaryGroup.tabs).toHaveLength(1); + } finally { + window.removeEventListener(TAB_EVENTS.EXPAND_RIGHT_PANEL, onRightPanelRequest); + } + }); + + it('drains the first file open into its target without expanding the session panel', async () => { + await act(async () => root.render()); + fileTabManager.openFile({ filePath: '/workspace/queued.ts', mode: 'project', sceneJustOpened: true }); + expect(useProjectCanvasStore.getState().primaryGroup.tabs).toHaveLength(0); + await act(async () => root.render()); + expect(useProjectCanvasStore.getState().primaryGroup.tabs).toHaveLength(1); + expect(appManager.getState().layout.rightPanelCollapsed).toBe(true); + }); + + it('reveals a session file for new and duplicate requests without broadcasting to standalone canvases', async () => { + await act(async () => root.render()); + const options = { filePath: '/workspace/session.ts', mode: 'agent' as const }; + await act(async () => { + fileTabManager.openFile(options); + fileTabManager.openFile(options); + }); + expect(appManager.getState().layout.rightPanelCollapsed).toBe(false); + expect(useAgentCanvasStore.getState().primaryGroup.tabs).toHaveLength(1); + expect(usePanelViewCanvasStore.getState().primaryGroup.tabs).toHaveLength(0); + expect(useProjectCanvasStore.getState().primaryGroup.tabs).toHaveLength(0); + + collapseSessionAuxPane(); + await act(async () => fileTabManager.openFile(options)); + expect(appManager.getState().layout.rightPanelCollapsed).toBe(false); + expect(useAgentCanvasStore.getState().primaryGroup.tabs).toHaveLength(1); + }); + + it('preserves manual collapse across content changes, workspace restore and host remount', async () => { + switchAgentCanvasWorkspace(null, 'workspace-a'); + useAgentCanvasStore.getState().addTab(content('session'), 'active'); + await act(async () => root.render()); + const tab = useAgentCanvasStore.getState().primaryGroup.tabs[0]; + await act(async () => useAgentCanvasStore.getState().updateTabContent(tab.id, 'primary', content('updated'))); + expect(appManager.getState().layout.rightPanelCollapsed).toBe(true); + await act(async () => { + switchAgentCanvasWorkspace('workspace-a', 'workspace-b'); + root.render(); + }); + await act(async () => { + switchAgentCanvasWorkspace('workspace-b', 'workspace-a'); + root.render(); + }); + expect(useAgentCanvasStore.getState().primaryGroup.tabs).toHaveLength(1); + expect(appManager.getState().layout.rightPanelCollapsed).toBe(true); + await act(async () => root.render(null)); + await act(async () => root.render()); + expect(appManager.getState().layout.rightPanelCollapsed).toBe(true); + }); + + it('keeps an explicitly opened empty panel and counts all editor groups when closing', async () => { + appManager.updateLayout({ rightPanelCollapsed: false }); + await act(async () => root.render()); + expect(appManager.getState().layout.rightPanelCollapsed).toBe(false); + await act(async () => { + const canvas = useAgentCanvasStore.getState(); + canvas.setSplitMode('grid'); + canvas.addTab(content('primary'), 'active', 'primary'); + canvas.addTab(content('tertiary'), 'active', 'tertiary'); + }); + await act(async () => useAgentCanvasStore.getState().closeAllTabs('primary')); + expect(appManager.getState().layout.rightPanelCollapsed).toBe(false); + // Closing a group can compact the remaining tabs into the primary group. + await act(async () => useAgentCanvasStore.getState().closeAllTabs()); + expect(appManager.getState().layout.rightPanelCollapsed).toBe(true); + }); + + it('preserves an open panel when workspace snapshots change before the shell rerenders', async () => { + switchAgentCanvasWorkspace(null, 'workspace-a'); + useAgentCanvasStore.getState().addTab(content('workspace-a'), 'active'); + appManager.updateLayout({ rightPanelCollapsed: false }); + await act(async () => root.render()); + await act(async () => switchAgentCanvasWorkspace('workspace-a', 'workspace-b')); + expect(useAgentCanvasStore.getState().workspaceKey).toBe('workspace-b'); + expect(useAgentCanvasStore.getState().primaryGroup.tabs).toHaveLength(0); + expect(appManager.getState().layout.rightPanelCollapsed).toBe(false); + await act(async () => switchAgentCanvasWorkspace('workspace-b', 'workspace-a')); + expect(useAgentCanvasStore.getState().primaryGroup.tabs).toHaveLength(1); + expect(appManager.getState().layout.rightPanelCollapsed).toBe(false); + }); + + it('keeps Git and bottom terminal operations scoped to their hosts', async () => { + await act(async () => root.render()); + await act(async () => { + window.dispatchEvent(new CustomEvent(TAB_EVENTS.GIT_CREATE_TAB, { detail: content('git-diff') })); + window.dispatchEvent(new CustomEvent(TAB_EVENTS.BOTTOM_TERMINAL_CREATE_TAB, { + detail: { type: 'terminal', title: 'terminal', data: { sessionId: 'terminal-1' }, + metadata: { terminalCloseBehavior: 'detach' } }, + })); + }); + expect(useGitCanvasStore.getState().primaryGroup.tabs).toHaveLength(1); + expect(appManager.getState().layout).toMatchObject({ + rightPanelCollapsed: true, bottomTerminalPanelCollapsed: false, + }); + await act(async () => useBottomTerminalCanvasStore.getState().closeAllTabs()); + expect(appManager.getState().layout).toMatchObject({ + rightPanelCollapsed: true, bottomTerminalPanelCollapsed: true, + }); + }); + + it('applies repeated reveal requests idempotently and leaves no deferred layout mutation after unmount', async () => { + await act(async () => root.render()); + await act(async () => { + window.dispatchEvent(new CustomEvent(TAB_EVENTS.EXPAND_RIGHT_PANEL)); + window.dispatchEvent(new CustomEvent(TAB_EVENTS.EXPAND_RIGHT_PANEL)); + window.dispatchEvent(new CustomEvent(TAB_EVENTS.EXPAND_BOTTOM_TERMINAL_PANEL)); + window.dispatchEvent(new CustomEvent(TAB_EVENTS.EXPAND_BOTTOM_TERMINAL_PANEL)); + }); + expect(appManager.getState().layout).toMatchObject({ + rightPanelCollapsed: false, bottomTerminalPanelCollapsed: false, + }); + await act(async () => root.render(null)); + collapseSessionAuxPane(); + collapseSessionBottomTerminalPane(); + window.dispatchEvent(new CustomEvent(TAB_EVENTS.EXPAND_RIGHT_PANEL)); + await act(async () => { await new Promise(resolve => requestAnimationFrame(() => resolve())); }); + expect(appManager.getState().layout).toMatchObject({ + rightPanelCollapsed: true, bottomTerminalPanelCollapsed: true, + }); + }); +}); diff --git a/src/web-ui/src/app/components/panels/content-canvas/hooks/useKeyboardShortcuts.ts b/src/web-ui/src/app/components/panels/content-canvas/hooks/useKeyboardShortcuts.ts index 5eccb308c2..474c6c54e9 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/hooks/useKeyboardShortcuts.ts +++ b/src/web-ui/src/app/components/panels/content-canvas/hooks/useKeyboardShortcuts.ts @@ -16,6 +16,7 @@ interface UseKeyboardShortcutsOptions { enabled?: boolean; missionControlEnabled?: boolean; handleCloseWithDirtyCheck?: (tabId: string, groupId: EditorGroupId) => Promise; + onReveal?: () => void; } export const useKeyboardShortcuts = (options: UseKeyboardShortcutsOptions = {}) => { @@ -23,6 +24,7 @@ export const useKeyboardShortcuts = (options: UseKeyboardShortcutsOptions = {}) enabled = true, missionControlEnabled = true, handleCloseWithDirtyCheck, + onReveal, } = options; const { @@ -33,6 +35,7 @@ export const useKeyboardShortcuts = (options: UseKeyboardShortcutsOptions = {}) closeTab, switchToTab, reopenClosedTab, + closedTabs, setSplitMode, setAnchorPosition, toggleMaximize, @@ -121,7 +124,11 @@ export const useKeyboardShortcuts = (options: UseKeyboardShortcutsOptions = {}) useShortcut( 'tab.reopenClosed', { key: 'T', ctrl: true, shift: true, scope: 'canvas', allowInInput: true }, - () => reopenClosedTab(), + () => { + if (closedTabs.length === 0) return; + reopenClosedTab(); + onReveal?.(); + }, { enabled, priority: 10, description: 'keyboard.shortcuts.tab.reopenClosed' } ); diff --git a/src/web-ui/src/app/components/panels/content-canvas/hooks/usePanelTabCoordinator.ts b/src/web-ui/src/app/components/panels/content-canvas/hooks/usePanelTabCoordinator.ts index 7031d58987..6a1449a19d 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/hooks/usePanelTabCoordinator.ts +++ b/src/web-ui/src/app/components/panels/content-canvas/hooks/usePanelTabCoordinator.ts @@ -1,183 +1,46 @@ -/** - * usePanelTabCoordinator Hook - * Tab and panel state coordinator. - * - * Responsibilities: - * 1. Watch tab count changes and manage panel expand/collapse - * 2. Sync panel state on tab open/close - * 3. Ensure state consistency and avoid race conditions - */ +import { useEffect, useRef } from 'react'; -import { useEffect, useRef, useCallback } from 'react'; -import { useCanvasStore } from '../stores'; -import { useApp } from '@/app/hooks/useApp'; -import { TAB_EVENTS } from '../types'; -import { loadPanelWidth, STORAGE_KEYS, RIGHT_PANEL_CONFIG } from '@/app/layout/panelConfig'; interface UsePanelTabCoordinatorOptions { - /** Auto-collapse when all tabs are closed */ - autoCollapseOnEmpty?: boolean; - /** Auto-expand when a tab opens */ - autoExpandOnTabOpen?: boolean; - /** Custom collapsed state for non-right-panel canvases. */ - isCollapsed?: boolean; - /** Custom expand behavior for non-right-panel canvases. */ - onExpand?: () => void; - /** Custom collapse behavior for non-right-panel canvases. */ - onCollapse?: () => void; - /** Event that requests panel expansion. Defaults to right-panel expansion. */ - expandEventName?: string; + visibleTabCount: number; + /** Changing workspace restores content without changing the user's layout. */ + scopeKey?: string; + expandEventName: string; + onExpand: () => void; + onCollapse: () => void; } /** - * Tab and panel state coordinator. - * - * Design principles: - * 1. Single source of truth: tabs from canvasStore, panels from useApp - * 2. Reactive sync: update panel state on tab changes - * 3. Event-driven: coordinate through unified events - * 4. Debounce: avoid frequent state updates + * A collapsible host owns its layout. Standalone canvases do not install this + * coordinator. Existing tabs, content updates and scene activation never imply + * an open request; only the host's explicit reveal action expands its panel. */ -export const usePanelTabCoordinator = (options: UsePanelTabCoordinatorOptions = {}) => { - const { - autoCollapseOnEmpty = true, - autoExpandOnTabOpen = true, - isCollapsed, - onExpand, - onCollapse, - expandEventName = TAB_EVENTS.EXPAND_RIGHT_PANEL, - } = options; - - const { - primaryGroup, - secondaryGroup, - } = useCanvasStore(); - - const { state, toggleRightPanel, updateRightPanelWidth } = useApp(); +export function usePanelTabCoordinator({ + visibleTabCount, + scopeKey, + expandEventName, + onExpand, + onCollapse, +}: UsePanelTabCoordinatorOptions) { + const previousRef = useRef({ visibleTabCount, scopeKey }); - // Use refs to avoid stale closures and add guards - const rightPanelCollapsedRef = useRef( - state?.layout?.rightPanelCollapsed ?? true - ); - const toggleRightPanelRef = useRef(toggleRightPanel); - const isInitializedRef = useRef(false); - - // Sync refs useEffect(() => { - if (typeof isCollapsed === 'boolean') { - rightPanelCollapsedRef.current = isCollapsed; - } else if (state?.layout) { - rightPanelCollapsedRef.current = state.layout.rightPanelCollapsed ?? true; - } - toggleRightPanelRef.current = toggleRightPanel; - // Mark initialized - if (!isInitializedRef.current) { - isInitializedRef.current = true; - } - }, [state?.layout, toggleRightPanel, isCollapsed]); - - /** - * Expand right panel (with debounce and state checks). - * Set width first, then expand to avoid flicker. - */ - const expandPanel = useCallback(() => { - if (onExpand) { - onExpand(); - return; - } - - if (rightPanelCollapsedRef.current && toggleRightPanelRef.current && updateRightPanelWidth) { - // Restore last width if available, otherwise use default - const lastWidth = loadPanelWidth(STORAGE_KEYS.RIGHT_PANEL_LAST_WIDTH, RIGHT_PANEL_CONFIG.COMFORTABLE_DEFAULT); - updateRightPanelWidth(lastWidth); - - // Expand immediately without animation (notify WorkspaceLayout) - window.dispatchEvent(new CustomEvent('expand-right-panel-immediate', { - detail: { noAnimation: true } - })); - - // Use requestAnimationFrame to run on next render - requestAnimationFrame(() => { - if (toggleRightPanelRef.current) { - toggleRightPanelRef.current(); - } - }); - } - }, [onExpand, updateRightPanelWidth]); - - /** - * Collapse right panel (with debounce and state checks). - */ - const collapsePanel = useCallback(() => { - if (onCollapse) { + const previous = previousRef.current; + previousRef.current = { visibleTabCount, scopeKey }; + if ( + previous.scopeKey === scopeKey + && previous.visibleTabCount > 0 + && visibleTabCount === 0 + ) { onCollapse(); - return; } + }, [visibleTabCount, scopeKey, onCollapse]); - if (!rightPanelCollapsedRef.current && toggleRightPanelRef.current) { - requestAnimationFrame(() => { - if (toggleRightPanelRef.current) { - toggleRightPanelRef.current(); - } - }); - } - }, [onCollapse]); - - /** - * Watch tab count changes and manage panel state. - */ - useEffect(() => { - // Wait until initialization completes - if (!isInitializedRef.current) { - return; - } - - // Count visible tabs - const primaryVisible = primaryGroup.tabs.filter(t => !t.isHidden).length; - const secondaryVisible = secondaryGroup.tabs.filter(t => !t.isHidden).length; - const visibleCount = primaryVisible + secondaryVisible; - - const isCollapsed = rightPanelCollapsedRef.current; - - // Auto-collapse when all tabs are closed - if (visibleCount === 0 && autoCollapseOnEmpty && !isCollapsed) { - collapsePanel(); - } - // Auto-expand when tabs exist - else if (visibleCount > 0 && autoExpandOnTabOpen && isCollapsed) { - expandPanel(); - } - }, [ - primaryGroup.tabs, - secondaryGroup.tabs, - autoCollapseOnEmpty, - autoExpandOnTabOpen, - expandPanel, - collapsePanel, - ]); - - /** - * Listen for expand-right-panel events. - * Backup mechanism to ensure panel expands for all tab-open paths. - */ + // Compatibility for callers that explicitly request this panel, including + // actions that reveal an existing tab without creating another one. useEffect(() => { - const handleExpandRightPanel = () => { - if (autoExpandOnTabOpen) { - expandPanel(); - } - }; - - window.addEventListener(expandEventName, handleExpandRightPanel); + window.addEventListener(expandEventName, onExpand); + return () => window.removeEventListener(expandEventName, onExpand); + }, [expandEventName, onExpand]); - return () => { - window.removeEventListener(expandEventName, handleExpandRightPanel); - }; - }, [autoExpandOnTabOpen, expandPanel, expandEventName]); - - return { - // Utilities - expandPanel, - collapsePanel, - }; -}; - -export default usePanelTabCoordinator; + return { expandPanel: onExpand, collapsePanel: onCollapse }; +} diff --git a/src/web-ui/src/app/components/panels/content-canvas/hooks/useTabLifecycle.ts b/src/web-ui/src/app/components/panels/content-canvas/hooks/useTabLifecycle.ts index 609c00a099..0844455548 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/hooks/useTabLifecycle.ts +++ b/src/web-ui/src/app/components/panels/content-canvas/hooks/useTabLifecycle.ts @@ -15,7 +15,9 @@ import { useProjectCanvasStore, useGitCanvasStore, useBottomTerminalCanvasStore, + usePanelViewCanvasStore, } from '../stores'; +import type { CanvasStoreMode } from '../stores/canvasStore'; import type { EditorGroupId, PanelContent, CreateTabEventDetail } from '../types'; import { TAB_EVENTS } from '../types'; import { useI18n } from '@/infrastructure/i18n'; @@ -27,11 +29,11 @@ import { destroyTerminalSession } from '@/shared/services/destroyTerminalSession const log = createLogger('useTabLifecycle'); interface UseTabLifecycleOptions { /** App mode / target canvas */ - mode?: 'agent' | 'project' | 'git' | 'bottom-terminal'; + mode?: CanvasStoreMode; /** Override the external tab creation event for specialized canvases. */ createTabEventName?: string; - /** Override the panel expansion event dispatched after tab activation. */ - expandPanelEventName?: string; + /** Only the containing host decides whether opening content reveals a panel. */ + onReveal?: () => void; } interface UseTabLifecycleReturn { @@ -61,7 +63,7 @@ export const useTabLifecycle = (options: UseTabLifecycleOptions = {}): UseTabLif const { mode = 'agent', createTabEventName, - expandPanelEventName = TAB_EVENTS.EXPAND_RIGHT_PANEL, + onReveal, } = options; const { t } = useI18n('components'); const canvasStoreApi = @@ -71,7 +73,9 @@ export const useTabLifecycle = (options: UseTabLifecycleOptions = {}): UseTabLif ? useGitCanvasStore : mode === 'bottom-terminal' ? useBottomTerminalCanvasStore - : useAgentCanvasStore; + : mode === 'panel-view' + ? usePanelViewCanvasStore + : useAgentCanvasStore; const { addTab, @@ -117,13 +121,15 @@ export const useTabLifecycle = (options: UseTabLifecycleOptions = {}): UseTabLif if (existing) { // Switch to existing tab switchToTab(existing.tab.id, existing.groupId); + onReveal?.(); return; } } // Add preview tab (auto-replaces current preview tab) addTab(content, 'preview', targetGroupId); - }, [activeGroupId, findTabByMetadata, switchToTab, addTab]); + onReveal?.(); + }, [activeGroupId, findTabByMetadata, switchToTab, addTab, onReveal]); /** * Open directly in active state. @@ -140,13 +146,15 @@ export const useTabLifecycle = (options: UseTabLifecycleOptions = {}): UseTabLif if (existing.tab.state === 'preview') { promoteTab(existing.tab.id, existing.groupId); } + onReveal?.(); return; } } // Add active tab addTab(content, 'active', targetGroupId); - }, [activeGroupId, findTabByMetadata, switchToTab, promoteTab, addTab]); + onReveal?.(); + }, [activeGroupId, findTabByMetadata, switchToTab, promoteTab, addTab, onReveal]); /** * Promote to active on edit. @@ -257,48 +265,41 @@ export const useTabLifecycle = (options: UseTabLifecycleOptions = {}): UseTabLif * Remove tabs when their terminal session is destroyed by any surface. */ useEffect(() => { - const store = mode === 'project' ? useProjectCanvasStore - : mode === 'git' ? useGitCanvasStore - : mode === 'bottom-terminal' ? useBottomTerminalCanvasStore - : useAgentCanvasStore; - const handleTerminalSessionDestroyed = (event: CustomEvent<{ sessionId: string }>) => { const { sessionId } = event.detail ?? {}; if (sessionId) { - store.getState().closeTerminalTabBySessionId(sessionId); + canvasStoreApi.getState().closeTerminalTabBySessionId(sessionId); } }; window.addEventListener('terminal-session-destroyed', handleTerminalSessionDestroyed as EventListener); return () => { window.removeEventListener('terminal-session-destroyed', handleTerminalSessionDestroyed as EventListener); }; - }, [mode]); + }, [canvasStoreApi]); /** * Keep terminal tab titles synchronized with session renames. */ useEffect(() => { - const store = mode === 'project' ? useProjectCanvasStore - : mode === 'git' ? useGitCanvasStore - : mode === 'bottom-terminal' ? useBottomTerminalCanvasStore - : useAgentCanvasStore; - const handleTerminalSessionRenamed = (event: CustomEvent<{ sessionId: string; newName: string }>) => { const { sessionId, newName } = event.detail ?? {}; if (sessionId && newName) { - store.getState().renameTerminalTabBySessionId(sessionId, newName); + canvasStoreApi.getState().renameTerminalTabBySessionId(sessionId, newName); } }; window.addEventListener('terminal-session-renamed', handleTerminalSessionRenamed as EventListener); return () => { window.removeEventListener('terminal-session-renamed', handleTerminalSessionRenamed as EventListener); }; - }, [mode]); + }, [canvasStoreApi]); /** * Listen for external tab creation events. */ useEffect(() => { + // Popped-out tabs are transferred directly into their independent store. + // This host must not also consume the session's tab-open requests. + if (mode === 'panel-view' && !createTabEventName) return; const eventName = createTabEventName ?? (mode === 'project' ? TAB_EVENTS.PROJECT_CREATE_TAB @@ -347,7 +348,7 @@ export const useTabLifecycle = (options: UseTabLifecycleOptions = {}): UseTabLif // Switch to existing tab switchToTab(existing.tab.id, existing.groupId); - window.dispatchEvent(new CustomEvent(expandPanelEventName)); + onReveal?.(); return; } } @@ -358,14 +359,14 @@ export const useTabLifecycle = (options: UseTabLifecycleOptions = {}): UseTabLif // Open all tabs in active state by default (no preview replacement) addTab(content, 'active', groupId); - window.dispatchEvent(new CustomEvent(expandPanelEventName)); + onReveal?.(); }; window.addEventListener(eventName, handleCreateTab as EventListener); // Drain any tab events that were enqueued before this listener was // registered (happens when the scene was just mounted for the first time). - if (mode !== 'bottom-terminal') { + if (mode !== 'bottom-terminal' && mode !== 'panel-view') { const pendingMode = mode === 'project' ? 'project' : mode === 'git' ? 'git' : 'agent'; const pending = drainPendingTabs(pendingMode); pending.forEach(detail => handleCreateTab({ detail } as CustomEvent)); @@ -374,7 +375,7 @@ export const useTabLifecycle = (options: UseTabLifecycleOptions = {}): UseTabLif return () => { window.removeEventListener(eventName, handleCreateTab as EventListener); }; - }, [mode, createTabEventName, expandPanelEventName, findTabByMetadata, updateTabContent, switchToTab, addTab, activeGroupId, layout.splitMode, setSplitMode]); + }, [mode, createTabEventName, onReveal, findTabByMetadata, updateTabContent, switchToTab, addTab, activeGroupId, layout.splitMode, setSplitMode]); return { openPreview, diff --git a/src/web-ui/src/app/components/panels/content-canvas/stores/canvasStore.ts b/src/web-ui/src/app/components/panels/content-canvas/stores/canvasStore.ts index 871183a30c..b7baf41e29 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/stores/canvasStore.ts +++ b/src/web-ui/src/app/components/panels/content-canvas/stores/canvasStore.ts @@ -30,6 +30,8 @@ import { normalizePath } from '@/shared/utils/pathUtils'; // ==================== Store State Types ==================== interface CanvasStoreState { + /** Live content scope, committed with workspace swaps and excluded from snapshots. */ + workspaceKey?: string; primaryGroup: EditorGroupState; secondaryGroup: EditorGroupState; tertiaryGroup: EditorGroupState; @@ -155,6 +157,7 @@ type CanvasStore = CanvasStoreState & CanvasStoreActions; // ==================== Initial State ==================== const initialState: CanvasStoreState = { + workspaceKey: undefined, primaryGroup: createEditorGroupState(), secondaryGroup: createEditorGroupState(), tertiaryGroup: createEditorGroupState(), @@ -1204,8 +1207,9 @@ function rememberAgentSnapshot(key: string, snapshot: CanvasStoreState): void { } } -function applyEmptyAgentCanvas(): void { +function applyEmptyAgentCanvas(workspaceKey?: string): void { useAgentCanvasStore.setState({ + workspaceKey, primaryGroup: createEditorGroupState(), secondaryGroup: createEditorGroupState(), tertiaryGroup: createEditorGroupState(), @@ -1259,6 +1263,7 @@ export function switchAgentCanvasWorkspace( if (nextSnapshotClone) { useAgentCanvasStore.setState({ + workspaceKey: to, primaryGroup: nextSnapshotClone.primaryGroup, secondaryGroup: nextSnapshotClone.secondaryGroup, tertiaryGroup: nextSnapshotClone.tertiaryGroup, @@ -1271,7 +1276,7 @@ export function switchAgentCanvasWorkspace( maxClosedTabsHistory: nextSnapshotClone.maxClosedTabsHistory, }); } else { - applyEmptyAgentCanvas(); + applyEmptyAgentCanvas(to); } lastAgentCanvasSwitchTargetKey = to; diff --git a/src/web-ui/src/app/components/scheduled-jobs/scheduledJobDraft.test.ts b/src/web-ui/src/app/components/scheduled-jobs/scheduledJobDraft.test.ts index 4b06ce2dd2..4715a25be9 100644 --- a/src/web-ui/src/app/components/scheduled-jobs/scheduledJobDraft.test.ts +++ b/src/web-ui/src/app/components/scheduled-jobs/scheduledJobDraft.test.ts @@ -27,7 +27,7 @@ function jobWithInterval(everyMs: number): CronJob { target: { kind: 'workspace', workspace: { workspacePath: '/tmp/workspace' }, - launch: { agentType: 'agentic' }, + launch: { agentType: 'Standard' }, }, createdAtMs: 0, configUpdatedAtMs: 0, @@ -64,7 +64,7 @@ describe('interval round trip', () => { ['half hourly', 30 * MINUTE_IN_MS], ['awkward', 90 * MINUTE_IN_MS], ])('rebuilds the exact interval for a %s job', (_label, everyMs) => { - const draft = jobToDraft(jobWithInterval(everyMs), 'agentic'); + const draft = jobToDraft(jobWithInterval(everyMs), 'Standard'); const schedule = buildScheduleFromDraft(draft); expect(schedule).toMatchObject({ kind: 'every', everyMs }); diff --git a/src/web-ui/src/app/components/scheduled-jobs/scheduledJobDraft.ts b/src/web-ui/src/app/components/scheduled-jobs/scheduledJobDraft.ts index 1aad7f48a0..023e962516 100644 --- a/src/web-ui/src/app/components/scheduled-jobs/scheduledJobDraft.ts +++ b/src/web-ui/src/app/components/scheduled-jobs/scheduledJobDraft.ts @@ -18,7 +18,7 @@ import { normalizePath } from '@/shared/utils/pathUtils'; export const MINUTE_IN_MS = 60_000; export const HOUR_IN_MS = 60 * MINUTE_IN_MS; export const DAY_IN_MS = 24 * HOUR_IN_MS; -export const DEFAULT_AGENT_TYPE = 'agentic'; +export const DEFAULT_AGENT_TYPE = 'Standard'; export const ASSISTANT_WORKSPACE_AGENT_TYPE = 'Claw'; /** diff --git a/src/web-ui/src/app/global-search/globalSearchArchitecture.test.ts b/src/web-ui/src/app/global-search/globalSearchArchitecture.test.ts index dd95bad5e8..7e20e87cb2 100644 --- a/src/web-ui/src/app/global-search/globalSearchArchitecture.test.ts +++ b/src/web-ui/src/app/global-search/globalSearchArchitecture.test.ts @@ -24,7 +24,7 @@ describe('global search ownership', () => { expect(globalSearch).toContain('variant="modal"'); expect(auxPane).not.toContain('GlobalSearchContent'); expect(auxPane).toContain('missionControlEnabled={false}'); - expect(contentCanvas).toContain(''); + expect(contentCanvas).toContain(''); expect(contentCanvas).toContain( 'onOpenMissionControl={missionControlEnabled ? handleOpenMissionControl : undefined}', ); diff --git a/src/web-ui/src/app/hooks/useAssistantBootstrap.test.tsx b/src/web-ui/src/app/hooks/useAssistantBootstrap.test.tsx index a454983a99..e6d2dab029 100644 --- a/src/web-ui/src/app/hooks/useAssistantBootstrap.test.tsx +++ b/src/web-ui/src/app/hooks/useAssistantBootstrap.test.tsx @@ -72,7 +72,7 @@ describe('assistant bootstrap draft', () => { readFileContent.mockClear(); await render(session({ sessionId: 'existing', totalTurnCount: 3 })); expect(readFileContent).not.toHaveBeenCalled(); - await render(session({ sessionId: 'code', mode: 'agentic' })); + await render(session({ sessionId: 'code', mode: 'Standard' })); expect(readFileContent).not.toHaveBeenCalled(); }); @@ -81,7 +81,7 @@ describe('assistant bootstrap draft', () => { await render(); expect(readFileContent).not.toHaveBeenCalled(); sessionComposerStore.getState().clearDraft('claw-1'); - await render(session({ sessionId: 'other', mode: 'agentic' })); + await render(session({ sessionId: 'other', mode: 'Standard' })); await render(); expect(readFileContent).not.toHaveBeenCalled(); expect(sessionComposerStore.getState().getDraft('claw-1').value).toBe(''); @@ -115,7 +115,7 @@ describe('assistant bootstrap draft', () => { let finish!: (content: string) => void; readFileContent.mockReturnValueOnce(new Promise(resolve => { finish = resolve; })); await render(); - await render(session({ sessionId: 'other', mode: 'agentic' })); + await render(session({ sessionId: 'other', mode: 'Standard' })); await act(async () => finish('# Bootstrap')); expect(sessionComposerStore.getState().getDraft('claw-1').value).toBe(''); expect(sessionComposerStore.getState().getDraft('other').value).toBe(''); diff --git a/src/web-ui/src/app/hooks/useDialogCompletionNotify.test.ts b/src/web-ui/src/app/hooks/useDialogCompletionNotify.test.ts index 43aca62670..bb93ee4f83 100644 --- a/src/web-ui/src/app/hooks/useDialogCompletionNotify.test.ts +++ b/src/web-ui/src/app/hooks/useDialogCompletionNotify.test.ts @@ -20,14 +20,14 @@ const session = (overrides: Partial = {}): Session => ({ status: 'idle', config: { modelName: 'gpt-test', - agentType: 'agentic', + agentType: 'Standard', }, createdAt: 1000, lastActiveAt: 1000, error: null, todos: [], maxContextTokens: 128128, - mode: 'agentic', + mode: 'Standard', workspacePath: '/workspace', parentSessionId: undefined, sessionKind: 'normal', diff --git a/src/web-ui/src/app/layout/FloatingMiniChat.scss b/src/web-ui/src/app/layout/FloatingMiniChat.scss index 842d165491..adb4fae3f6 100644 --- a/src/web-ui/src/app/layout/FloatingMiniChat.scss +++ b/src/web-ui/src/app/layout/FloatingMiniChat.scss @@ -104,10 +104,6 @@ $panel-height: 680px; place-items: center; } - &:active { - transform: scale(0.97); - } - &:focus-visible { inline-size: var(--openbitfun-control-launcher-button-min-inline-size); transition-duration: 0s; @@ -180,18 +176,12 @@ $panel-height: 680px; transition: color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), background-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - transform var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); + border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); &:hover { color: var(--openbitfun-color-content-primary); background: var(--openbitfun-color-action-neutral-surface-hover); border-color: var(--openbitfun-color-border-strong); - transform: scale(1.06); - } - - &:active { - transform: scale(0.96); } } @@ -636,7 +626,6 @@ $panel-height: 680px; } &:active { - transform: scale(0.985); background: var(--openbitfun-color-action-neutral-surface-hover); } diff --git a/src/web-ui/src/app/scenes/agents/AgentsScene.scss b/src/web-ui/src/app/scenes/agents/AgentsScene.scss index d211dc188d..9cdb3b90f5 100644 --- a/src/web-ui/src/app/scenes/agents/AgentsScene.scss +++ b/src/web-ui/src/app/scenes/agents/AgentsScene.scss @@ -1,3 +1,8 @@ +// Keep controls aligned with their surfaces while retaining color and label motion. +:is(.openbitfun-agents-scene, .agent-detail-modal) :is(button, [role='button']):active { + scale: none; +} + .openbitfun-agents-scene { flex: 1 1 auto; width: 100%; @@ -13,8 +18,8 @@ } .gallery-page-header { - align-items: flex-start; - padding-top: 28px; + align-items: center; + padding-top: var(--openbitfun-space-6); padding-bottom: var(--openbitfun-space-4); } @@ -28,29 +33,43 @@ } .gallery-grid { - gap: 10px; + gap: var(--openbitfun-space-3); } .gallery-zone__subtitle { - font-size: var(--openbitfun-type-label-md-font-size); - color: var(--openbitfun-color-content-secondary); + font-size: var(--openbitfun-type-body-sm-font-size); + color: var(--openbitfun-color-content-muted); + line-height: var(--openbitfun-type-body-sm-line-height); + } + + &__catalog-toolbar { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--openbitfun-space-3); + min-width: 0; } &__search { - width: min(280px, 34vw); + flex: 1 1 220px; + width: 100%; + min-width: 0; + max-width: 320px; } &__agent-filters { display: flex; align-items: center; - gap: var(--openbitfun-space-4); + gap: var(--openbitfun-space-3); flex-wrap: wrap; + margin-inline-start: auto; } &__agent-filter-group { display: flex; align-items: center; - gap: var(--openbitfun-space-1); + gap: var(--openbitfun-space-2); + min-width: 0; } &__agent-filter-label { @@ -80,114 +99,83 @@ } .gallery-zone__heading { - gap: var(--openbitfun-space-2); + gap: var(--openbitfun-space-1); } .gallery-zone__subtitle { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-4); max-width: 60ch; - font-size: var(--openbitfun-type-body-md-font-size); - line-height: var(--openbitfun-type-body-lg-line-height); } } - &__harness-caption { - margin: 0; - color: var(--openbitfun-color-content-muted); - font-family: var(--openbitfun-type-micro-font-family); - font-size: var(--openbitfun-type-micro-font-size); - font-weight: var(--openbitfun-type-micro-font-weight); - line-height: var(--openbitfun-type-micro-line-height); - letter-spacing: var(--openbitfun-type-micro-letter-spacing); - text-transform: uppercase; - } - &__harness-presentation { - min-height: 112px; - padding: var(--openbitfun-space-4) var(--openbitfun-space-5); - border-radius: var(--openbitfun-layout-field-group-radius); - background: var(--openbitfun-color-surface-tertiary); - display: grid; - grid-template-columns: 52px clamp(52px, 8cqi, 88px) minmax(0, 1fr) clamp(52px, 8cqi, 88px) 52px; - align-items: center; - column-gap: var(--openbitfun-space-2); - min-width: 0; - } - - &__harness-endpoint { - display: grid; - place-items: center; - width: 52px; - height: 52px; - box-sizing: border-box; - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-pill); + margin: 0; + padding: 0; + list-style: none; + border: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); + border-radius: var(--openbitfun-layout-card-radius-md); background: var(--openbitfun-color-surface-panel); - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); + overflow: hidden; + min-width: 0; } - &__harness-connections { - position: relative; - align-self: stretch; + &__harness-profile-item { min-width: 0; - color: var(--openbitfun-color-field-border-hover); + margin: 0; - svg { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - fill: none; - stroke: currentColor; - stroke-width: 1.25; + // The outer list clips the fill once, against its own border contour. + &:has(> .openbitfun-agents-scene__harness-profile:hover:not(:disabled)) { + background-color: var(--openbitfun-color-surface-subtle); } - } - &__harness-connection-arrow { - display: none; - } - - &__harness-result-arrow { - position: absolute; - top: 50%; - right: 0; - transform: translateY(-50%); - } - - &__harness-profiles { - display: grid; - // All strategies share content-sized labels and one aligned route column. - grid-template-columns: 24px max-content fit-content(160px) minmax(84px, 1fr); - grid-template-rows: repeat(4, minmax(44px, 1fr)); - column-gap: var(--openbitfun-space-3); - min-width: 0; + & + & { + border-top: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); + } } &__harness-profile { + background: transparent; + border: 0; + border-radius: 0; + padding: var(--openbitfun-space-3) var(--openbitfun-space-4); + width: 100%; + min-height: 72px; + box-sizing: border-box; + font: inherit; + text-align: start; + cursor: pointer; display: grid; - grid-column: 1 / -1; - grid-template-columns: subgrid; + grid-template-columns: 24px 120px minmax(0, 1fr) clamp(160px, 28cqi, 280px); align-items: center; - column-gap: inherit; + column-gap: var(--openbitfun-space-4); min-width: 0; - &[data-openbitfun-profile='minimal'] { + &:hover:not(:disabled) .openbitfun-agents-scene__harness-route-node, + &:hover:not(:disabled) .openbitfun-agents-scene__harness-route-arrow { + background: var(--openbitfun-color-surface-subtle); + } + + &:focus-visible { + outline: var(--openbitfun-focus-width) solid var(--openbitfun-color-focus-ring); + outline-offset: calc(-1 * var(--openbitfun-focus-width)); + } + + &:disabled { + cursor: default; + } + + &[data-openbitfun-profile='Minimal'] { color: var(--openbitfun-color-identity-harness-minimal); } - &[data-openbitfun-profile='balanced'] { + &[data-openbitfun-profile='Standard'] { color: var(--openbitfun-color-identity-harness-standard); } - &[data-openbitfun-profile='ultimate'] { + &[data-openbitfun-profile='Ultimate'] { color: var(--openbitfun-color-identity-harness-ultimate); } - &[data-openbitfun-profile='creative'] { + &[data-openbitfun-profile='Creative'] { color: var(--openbitfun-color-identity-harness-creative); } } @@ -197,9 +185,11 @@ } &__harness-profile-name { + min-width: 0; color: var(--openbitfun-color-content-primary); font-size: var(--openbitfun-type-label-md-font-size); font-weight: var(--openbitfun-type-label-selected-font-weight); + overflow-wrap: anywhere; } &__harness-profile-purpose { @@ -232,7 +222,7 @@ display: grid; place-items: center; color: inherit; - background: var(--openbitfun-color-surface-tertiary); + background: var(--openbitfun-color-surface-panel); transform: translate(-50%, -50%); } @@ -254,24 +244,6 @@ } } -@container agents-scene (max-width: 1080px) { - .openbitfun-agents-scene { - .gallery-page-header { - flex-direction: column; - align-items: flex-start; - } - - .gallery-page-header__actions { - width: 100%; - flex-wrap: wrap; - } - - &__search { - width: min(360px, 100%); - } - } -} - @container agents-scene (max-width: 720px) { .openbitfun-agents-scene { .gallery-page-header { @@ -279,24 +251,16 @@ padding-bottom: var(--openbitfun-space-3); } - .gallery-page-header__actions { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - } - .gallery-zones { gap: var(--openbitfun-space-5); padding-top: var(--openbitfun-space-2); } - .gallery-zone__tools { - justify-content: flex-start; - } - &__agent-filters { width: 100%; align-items: flex-start; gap: var(--openbitfun-space-2); + margin-inline-start: 0; } &__agent-filter-group { @@ -310,48 +274,24 @@ } &__search { - width: 100%; - } - } -} - -@container agent-harness (max-width: 980px) { - .openbitfun-agents-scene { - &__harness-zone.gallery-zone .gallery-zone__subtitle { - gap: var(--openbitfun-space-2); + flex-basis: 100%; + max-width: none; } } } @container agent-harness (max-width: 680px) { .openbitfun-agents-scene { - &__harness-presentation { - grid-template-columns: 44px 36px minmax(0, 1fr) 36px 44px; - column-gap: var(--openbitfun-space-1); - } - - &__harness-endpoint { - width: 44px; - height: 44px; - } - - &__harness-profiles { - grid-template-columns: 20px minmax(100px, 1fr) minmax(64px, 1fr); - grid-template-rows: repeat(4, minmax(52px, 1fr)); - column-gap: var(--openbitfun-space-2); - } - &__harness-profile { + grid-template-columns: 24px minmax(0, 1fr) minmax(120px, 32%); grid-template-rows: auto auto; - row-gap: 0; - padding-block: var(--openbitfun-space-1); + column-gap: var(--openbitfun-space-3); + row-gap: var(--openbitfun-space-1); } &__harness-profile-icon { grid-column: 1; grid-row: 1 / 3; - width: 20px; - height: 20px; } &__harness-profile-name { @@ -375,32 +315,15 @@ @container agent-harness (max-width: 480px) { .openbitfun-agents-scene { - &__harness-presentation { - padding: var(--openbitfun-space-4); - grid-template-columns: minmax(0, 1fr); - gap: var(--openbitfun-space-1); - } - - &__harness-endpoint { - justify-self: center; - } - - &__harness-connections { - display: grid; - place-items: center; - height: 16px; - - svg { - display: none; - } - } - - &__harness-connection-arrow { - display: inline-block; + &__harness-profile { + grid-template-columns: 24px minmax(0, 1fr); + padding-inline: var(--openbitfun-space-3); } - &__harness-result-arrow { - display: none; + &__harness-route { + grid-column: 2; + grid-row: 3; + margin-top: var(--openbitfun-space-2); } } } diff --git a/src/web-ui/src/app/scenes/agents/AgentsScene.test.tsx b/src/web-ui/src/app/scenes/agents/AgentsScene.test.tsx index 507db7b64e..c8fffb3d7f 100644 --- a/src/web-ui/src/app/scenes/agents/AgentsScene.test.tsx +++ b/src/web-ui/src/app/scenes/agents/AgentsScene.test.tsx @@ -50,8 +50,8 @@ vi.mock('./components/AgentCard', () => ({ ), })); -vi.mock('./components/CoreAgentCard', () => ({ - default: () =>
, +vi.mock('./components/CoreAgentCard', async () => ({ + default: (await import('./components/AgentCard')).default, })); vi.mock('./components/useUserToolGroups', () => ({ @@ -62,11 +62,12 @@ vi.mock('./components/useUserToolGroups', () => ({ }), })); -vi.mock('./components/useUserSkillGroups', () => ({ +vi.mock('@/features/skill-groups/useUserSkillGroups', () => ({ useUserSkillGroups: () => ({ groups: [], loading: false, - saveGroups: vi.fn(), + error: null, + reload: vi.fn(), }), })); @@ -124,6 +125,7 @@ vi.mock('./hooks/useAgentsList', () => ({ function mockAgentsList(overrides: Record = {}) { useAgentsListMock.mockReturnValue({ + hiddenAgentIds: new Set(), allAgents: [], filteredAgents: [], loading: false, @@ -268,72 +270,19 @@ describeWithJsdom('AgentsScene', () => { expect(stylesheet).toContain('min-width: 0;'); }); - it('uses one compact responsive catalog without overview category navigation', () => { + it('uses one responsive catalog without overview category navigation', () => { const sceneSource = readFileSync( fileURLToPath(new URL('./AgentsScene.tsx', import.meta.url)), 'utf8', ); - const agentCardStyles = readFileSync( - fileURLToPath(new URL('./components/AgentCard.scss', import.meta.url)), - 'utf8', - ); - const coreCardSurfaceStyles = readFileSync( - fileURLToPath(new URL('./components/_AgentSurfaceCard.scss', import.meta.url)), - 'utf8', - ); - const coreCardStyles = readFileSync( - fileURLToPath(new URL('./components/CoreAgentCard.scss', import.meta.url)), - 'utf8', - ); - const agentCardSource = readFileSync( - fileURLToPath(new URL('./components/AgentCard.tsx', import.meta.url)), - 'utf8', - ); - const coreCardSource = readFileSync( - fileURLToPath(new URL('./components/CoreAgentCard.tsx', import.meta.url)), - 'utf8', - ); expect(sceneSource.match(/]*\bminCardWidth=\{300\}[^>]*>/g)).toHaveLength(1); expect(sceneSource).toContain('catalogAgents.map'); expect(sceneSource).not.toContain('gallery-anchor-bar'); expect(sceneSource).not.toContain('agents-core-zone'); - expect(agentCardStyles).toMatch(/\.agent-card \{\s+width: 100%;\s+min-width: 0;/); - expect(coreCardSurfaceStyles).toMatch(/width: 100%;\s+min-width: 0;/); - expect(agentCardStyles).toContain('height: 148px;'); - expect(coreCardSurfaceStyles).toContain('height: 148px;'); - expect(agentCardStyles).toContain('border-radius: var(--openbitfun-layout-field-group-radius);'); - expect(coreCardSurfaceStyles).toContain('border-radius: var(--openbitfun-layout-field-group-radius);'); - expect(agentCardStyles).toContain('background: var(--openbitfun-color-surface-tertiary);'); - expect(coreCardSurfaceStyles).toContain('background: var(--openbitfun-color-surface-tertiary);'); - expect(agentCardStyles).not.toContain('box-shadow: var(--openbitfun-shadow-xs);'); - expect(coreCardSurfaceStyles).not.toContain('box-shadow: var(--openbitfun-shadow-xs);'); - expect(agentCardStyles).toContain('grid-template-columns: 56px minmax(0, 1fr);'); - expect(coreCardStyles).toContain('grid-template-columns: 56px minmax(0, 1fr);'); - expect(agentCardStyles).toContain('inset-block: 12px;'); - expect(coreCardStyles).toContain('inset-block: 12px;'); - expect(agentCardStyles).toContain('@container agent-card (max-width: 330px)'); - expect(coreCardStyles).toContain('@container core-agent-card (max-width: 330px)'); - expect(agentCardSource).toContain('agent-card__icon-area'); - expect(agentCardSource).toContain('agent-card__dot-field'); - expect(agentCardSource).toContain("t('agentCard.metrics.collaboration')"); - expect(coreCardSource).toContain("t('agentCard.status.connected')"); - expect(coreCardSource).toContain('core-agent-card__status'); - expect(coreCardSource).toContain('core-agent-card__dot-field'); - expect(agentCardSource).not.toContain('CAPABILITY_ACCENT'); - expect(agentCardSource).not.toContain('--agent-card-gradient'); - expect(coreCardSource).not.toContain('getAlphaColor'); - expect(coreCardSource).not.toContain('--core-card-gradient'); - expect(coreCardStyles).toMatch(/&__status \{[\s\S]*?color: var\(--openbitfun-color-content-primary\);[\s\S]*?\.core-agent-card__status-icon \{[\s\S]*?color: var\(--openbitfun-color-status-success-content\);/); - expect(coreCardSurfaceStyles).not.toContain('$gradient'); - expect(coreCardSurfaceStyles).toContain('@mixin agent-icon-dot-field()'); - expect(coreCardSurfaceStyles).not.toContain('background-size: 7px 7px;'); - expect(coreCardSurfaceStyles).toContain('display: none;'); - expect(agentCardStyles).not.toContain('width: 360px;'); - expect(coreCardSurfaceStyles).not.toContain('width: 360px;'); }); - it('presents four Harness strategies as descriptive content between task and result', async () => { + it('presents four canonical Harness entries as a list', async () => { const { default: AgentsScene } = await import('./AgentsScene'); await act(async () => { @@ -341,21 +290,24 @@ describeWithJsdom('AgentsScene', () => { }); const presentation = container.querySelector('.openbitfun-agents-scene__harness-presentation'); - expect(presentation?.getAttribute('aria-label')).toBe('harnessZone.flowCaption'); + expect(presentation?.tagName).toBe('UL'); + expect(presentation?.getAttribute('role')).toBe('list'); + expect(presentation?.getAttribute('aria-label')).toBe('harnessZone.title'); + expect(presentation?.querySelectorAll(':scope > li')).toHaveLength(4); expect(presentation?.querySelectorAll('[data-openbitfun-component="harness-profile-step"]')).toHaveLength(4); - expect(Array.from(presentation?.querySelectorAll('.openbitfun-agents-scene__harness-endpoint') ?? []) - .map(node => node.textContent)).toEqual(['harnessZone.task', 'harnessZone.result']); - for (const id of ['minimal', 'balanced', 'ultimate', 'creative']) { + for (const id of ['Minimal', 'Standard', 'Ultimate', 'Creative']) { const profile = container.querySelector(`[data-testid="agents-harness-${id}"]`); expect(profile?.dataset.openbitfunProfile).toBe(id); expect(profile?.textContent).toContain(`harnessZone.profiles.${id}.name`); expect(profile?.textContent).toContain(`harnessZone.profiles.${id}.purpose`); - expect(profile?.tagName).toBe('DIV'); + expect(profile?.tagName).toBe('BUTTON'); + expect(profile?.querySelector('.openbitfun-agents-scene__harness-profile-icon')).not.toBeNull(); + expect(profile?.querySelector('.openbitfun-agents-scene__harness-route svg')).not.toBeNull(); expect(profile?.dataset.openbitfunState).toBeUndefined(); } - expect(presentation?.querySelector('button, [role="button"], [tabindex]')).toBeNull(); - expect(presentation?.textContent).not.toMatch(/harnessZone\.(connected|comingSoon)/); + expect(presentation?.querySelectorAll('button')).toHaveLength(4); + expect(presentation?.textContent).not.toMatch(/harnessZone\.(task|result|connected|comingSoon)/); expect(notificationInfoMock).not.toHaveBeenCalled(); expect(notificationSuccessMock).not.toHaveBeenCalled(); @@ -429,7 +381,7 @@ describeWithJsdom('AgentsScene', () => { defaultEnabled: true, effectiveEnabled: true, source: 'user', - agentKind: 'mode' as const, + agentKind: 'agent' as const, capabilities: [], }; mockAgentsList({ @@ -444,7 +396,7 @@ describeWithJsdom('AgentsScene', () => { }, ], getModeConfig: () => ({ - profile_id: 'coding_shared', + profile_id: 'Standard', enabled_tools: ['Read', 'mcp__github__list_issues'], default_tools: ['Read'], }), @@ -488,7 +440,7 @@ describeWithJsdom('AgentsScene', () => { defaultEnabled: true, effectiveEnabled: true, source: 'user', - agentKind: 'mode' as const, + agentKind: 'agent' as const, capabilities: [], }; mockAgentsList({ diff --git a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx index 01a0fceabc..b2361dabe0 100644 --- a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx +++ b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx @@ -1,4 +1,9 @@ -import { OverflowText, Button, Combobox, Icon, IconButton, SearchField, Select, StatusPill, Tooltip, ScrollArea, type IconSource } from '@openbitfun/ui'; +import { isPrimaryAgent, isOrdinaryAgent } from './agentVisibility'; +import { + OverflowText, Button, Combobox, FormSection, Icon, IconButton, + NavigationPanelItem, NavigationPanelSeparator, SearchField, Select, + StatusPill, Tooltip, ScrollArea, type IconSource, +} from '@openbitfun/ui'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { TFunction } from 'i18next'; import { Bot, Cpu, FileText, MessageSquareText, RotateCcw, Wrench } from 'lucide-react'; @@ -16,7 +21,7 @@ import { } from '@/app/components'; import AgentCard from './components/AgentCard'; import AgentHarnessOverview from './components/AgentHarnessOverview'; -import CoreAgentCard, { type CoreAgentMeta } from './components/CoreAgentCard'; +import CoreAgentCard from './components/CoreAgentCard'; import CreateAgentPage from './components/CreateAgentPage'; import { AgentCapabilityTooltip, @@ -25,7 +30,7 @@ import { import { capabilityTooltipAriaLabel } from './components/agentCapabilityTooltipUtils'; import { SkillGroupPicker, SkillGroupSummary } from './components/SkillGroupPicker'; import { ToolGroupPicker, ToolGroupSummary } from './components/ToolGroupPicker'; -import { useUserSkillGroups } from './components/useUserSkillGroups'; +import { useUserSkillGroups } from '@/features/skill-groups/useUserSkillGroups'; import { useUserToolGroups } from './components/useUserToolGroups'; import { type AgentFilterLevel, @@ -35,15 +40,14 @@ import { } from './agentsStore'; import { useAgentsList } from './hooks/useAgentsList'; import { AGENT_ICON_MAP } from './agentsIcons'; -import { CAPABILITY_ACCENT, CORE_AGENT_ACCENTS, DEFAULT_CORE_AGENT_ACCENT } from './agentAppearance'; -import { getCardGradient } from '@/shared/utils/cardGradients'; +import { CAPABILITY_ACCENT, DEFAULT_CORE_AGENT_ACCENT } from './agentAppearance'; import { isAgentProfileConfigurableToolName } from './agentToolVisibility'; import { getAgentBadge, getAgentDescription, getCapabilityLabel } from './utils'; import './AgentsView.scss'; import './AgentsScene.scss'; +import './components/AgentCard.scss'; import { useGallerySceneAutoRefresh } from '@/app/hooks/useGallerySceneAutoRefresh'; import { - CORE_AGENT_IDS, isAgentInOverviewZone, isLocallyManageableSubagent, } from './agentVisibility'; @@ -199,7 +203,8 @@ const AgentsHomeView: React.FC = () => { } = useUserToolGroups(); const { groups: userSkillGroups, - saveGroups: saveUserSkillGroups, + error: skillGroupsError, + reload: reloadSkillGroups, } = useUserSkillGroups(); const { @@ -245,27 +250,15 @@ const AgentsHomeView: React.FC = () => { sceneId: 'agents', refetch: () => { void loadAgents(); + void reloadSkillGroups(); }, }); - const coreAgentMeta = useMemo((): Record => ({ - agentic: { - role: t('coreAgentsZone.modes.agentic.role'), - ...CORE_AGENT_ACCENTS.agentic, - }, - Cowork: { - role: t('coreAgentsZone.modes.cowork.role'), - ...CORE_AGENT_ACCENTS.Cowork, - }, - ComputerUse: { - role: t('coreAgentsZone.modes.computerUse.role'), - ...CORE_AGENT_ACCENTS.ComputerUse, - }, - }), [t]); + const coreAgents = useMemo( - () => filteredAgents.filter((agent) => CORE_AGENT_IDS.has(agent.id)), - [filteredAgents], + () => filteredAgents.filter((agent) => isOrdinaryAgent(agent) && !hiddenAgentIds.has(agent.id)), + [filteredAgents, hiddenAgentIds], ); const visibleAgents = useMemo( @@ -288,14 +281,14 @@ const AgentsHomeView: React.FC = () => { const typeFilterOptions = useMemo(() => [ { value: 'all', label: t('filters.anyKind') }, - { value: 'mode', label: t('filters.mode') }, + { value: 'agent', label: t('filters.agent') }, { value: 'subagent', label: t('filters.subagent') }, ], [t]); const renderSkeletons = (prefix: string) => ( @@ -309,11 +302,11 @@ const AgentsHomeView: React.FC = () => { selectedAgent?.source ?? selectedAgent?.subagentSource ) === 'external'; const selectedAgentModeConfig = useMemo( - () => (selectedAgent?.agentKind === 'mode' ? getModeConfig(selectedAgent.id) : null), + () => (isPrimaryAgent(selectedAgent) ? getModeConfig(selectedAgent.id) : null), [getModeConfig, selectedAgent], ); const selectedAgentModeProfile = useMemo( - () => (selectedAgent?.agentKind === 'mode' ? getModeProfile(selectedAgent.id) : null), + () => (isPrimaryAgent(selectedAgent) ? getModeProfile(selectedAgent.id) : null), [getModeProfile, selectedAgent], ); const selectedAgentSkillConfigs = useMemo( @@ -321,7 +314,7 @@ const AgentsHomeView: React.FC = () => { [getAgentSkills, selectedAgent], ); const selectedAgentManageableSubagents = useMemo( - () => (selectedAgent?.agentKind === 'mode' ? getModeManageableSubagents(selectedAgent.id) : []), + () => (isPrimaryAgent(selectedAgent) ? getModeManageableSubagents(selectedAgent.id) : []), [getModeManageableSubagents, selectedAgent], ); const selectedAgentEditableSubagents = useMemo( @@ -329,7 +322,7 @@ const AgentsHomeView: React.FC = () => { [selectedAgentManageableSubagents], ); const selectedAgentConfiguredTools = useMemo(() => ( - selectedAgent?.agentKind === 'mode' + isPrimaryAgent(selectedAgent) ? (selectedAgentModeConfig?.enabled_tools ?? selectedAgent.defaultTools ?? []) : (selectedAgent?.defaultTools ?? []) ), [selectedAgent, selectedAgentModeConfig]); @@ -342,7 +335,7 @@ const AgentsHomeView: React.FC = () => { [availableTools], ); const selectedAgentHasSkillTool = hasSkillTool(selectedAgentConfiguredTools); - const selectedAgentHasTaskTool = selectedAgent?.agentKind === 'mode' + const selectedAgentHasTaskTool = isPrimaryAgent(selectedAgent) ? hasTaskTool(selectedAgentConfiguredTools) : false; const selectedAgentEnabledSubagents = useMemo( @@ -387,12 +380,12 @@ const AgentsHomeView: React.FC = () => { } return selectedAgentModeProfile.memberModeIds.map((memberId) => ( - allAgents.find((agent) => agent.agentKind === 'mode' && agent.id === memberId)?.name ?? memberId + allAgents.find((agent) => isPrimaryAgent(agent) && agent.id === memberId)?.name ?? memberId )); }, [allAgents, selectedAgentModeProfile]); const selectedAgentUsesSharedProfile = (selectedAgentModeProfile?.memberModeIds.length ?? 0) > 1; const getDisplayedToolCount = useCallback((agent: AgentWithCapabilities): number => { - const configuredTools = agent.agentKind === 'mode' + const configuredTools = isPrimaryAgent(agent) ? (getModeConfig(agent.id)?.enabled_tools ?? agent.defaultTools) : agent.defaultTools; if (configuredTools) { @@ -401,7 +394,7 @@ const AgentsHomeView: React.FC = () => { return agent.toolCount ?? 0; }, [getModeConfig]); const getDisplayedSkillCount = useCallback((agent: AgentWithCapabilities): number => { - const configuredTools = agent.agentKind === 'mode' + const configuredTools = isPrimaryAgent(agent) ? (getModeConfig(agent.id)?.enabled_tools ?? agent.defaultTools ?? []) : (agent.defaultTools ?? []); return hasSkillTool(configuredTools) @@ -409,7 +402,7 @@ const AgentsHomeView: React.FC = () => { : 0; }, [getAgentSkills, getModeConfig]); const getDisplayedSubagentCount = useCallback((agent: AgentWithCapabilities): number => { - if (agent.agentKind !== 'mode') { + if (!isPrimaryAgent(agent)) { return 0; } const configuredTools = getModeConfig(agent.id)?.enabled_tools ?? agent.defaultTools ?? []; @@ -484,12 +477,12 @@ const AgentsHomeView: React.FC = () => { } if (selectedAgentTools.length > 0) { - const currentToolCount = selectedAgent?.agentKind === 'mode' + const currentToolCount = isPrimaryAgent(selectedAgent) ? (toolsEditing ? (pendingTools ?? selectedAgentTools).length : selectedAgentTools.length) : selectedAgentTools.length; - const totalToolCount = selectedAgent?.agentKind === 'mode' + const totalToolCount = isPrimaryAgent(selectedAgent) ? agentProfileAvailableTools.length : selectedAgentTools.length; @@ -497,7 +490,7 @@ const AgentsHomeView: React.FC = () => { key: 'tools', icon: { glyph: Wrench }, label: t('agentsOverview.tools'), - count: selectedAgent?.agentKind === 'mode' + count: isPrimaryAgent(selectedAgent) ? `${currentToolCount}/${totalToolCount}` : `${currentToolCount}`, }); @@ -506,7 +499,7 @@ const AgentsHomeView: React.FC = () => { if (selectedAgentHasSkillTool && selectedAgentSkillConfigs.length > 0) { const currentSkillCount = skillsEditing ? (pendingSkills ?? selectedAgentSkills).length - : selectedAgent?.agentKind === 'mode' + : isPrimaryAgent(selectedAgent) ? selectedAgentRuntimeSkillCount : selectedAgentSkills.length; tabs.push({ @@ -517,7 +510,7 @@ const AgentsHomeView: React.FC = () => { }); } - if (selectedAgent?.agentKind === 'mode' && selectedAgentHasTaskTool) { + if (isPrimaryAgent(selectedAgent) && selectedAgentHasTaskTool) { const currentSubagentIds = subagentsEditing ? (pendingSubagentIds ?? selectedAgentEnabledSubagentIds) : selectedAgentEnabledSubagentIds; @@ -559,7 +552,7 @@ const AgentsHomeView: React.FC = () => { const currentCapabilityMeta = selectedAgentCapabilityTabs.find( (tab) => tab.key === currentCapabilityTab, ); - const canManageCurrentCapability = selectedAgent?.agentKind === 'mode' + const canManageCurrentCapability = isPrimaryAgent(selectedAgent) || ( currentCapabilityTab === 'skills' && selectedAgent?.agentKind === 'subagent' @@ -655,7 +648,28 @@ const AgentsHomeView: React.FC = () => { title={t('page.title')} subtitle={t('page.subtitle')} actions={( - <> + + )} + /> + +
+ agent.agentKind === 'harness')} onOpenDetails={openAgentDetails} /> + + {catalogAgents.length} : null} + > +
{ onClear={searchQuery ? () => setSearchQuery('') : undefined} data-testid="agents-search" /> - - - )} - /> - -
- - - -
-
- - {t('filters.source')} - - setAgentFilterType( - normalizeSelectValue(value) as AgentFilterType, - )} - /> -
+
+
+ + {t('filters.source')} + + setAgentFilterType( + normalizeSelectValue(value) as AgentFilterType, + )} + /> +
+
+
+ {loading ? renderSkeletons('agent') : null} {!loading && catalogAgents.length === 0 ? ( @@ -749,30 +738,27 @@ const AgentsHomeView: React.FC = () => { data-openbitfun-scene="agents" data-openbitfun-part="catalogGrid" > - {catalogAgents.map((agent, index) => { + {catalogAgents.map((agent) => { const commonCardProps = { agent, - index, toolCount: getDisplayedToolCount(agent), skillCount: getDisplayedSkillCount(agent), subagentCount: getDisplayedSubagentCount(agent), onOpenDetails: openAgentDetails, + disabledReason: agent.id === 'ComputerUse' && !computerUseEnabled + ? t('coreAgentsZone.computerUseDisabledBadge') + : undefined, }; - if (CORE_AGENT_IDS.has(agent.id)) { + if (isOrdinaryAgent(agent)) { return ( ); } @@ -787,10 +773,10 @@ const AgentsHomeView: React.FC = () => { : } - iconGradient={selectedAgent ? getCardGradient(selectedAgent.id || selectedAgent.name) : undefined} title={selectedAgent?.name ?? ''} titlePlacement="hero" size="2xl" @@ -799,7 +785,7 @@ const AgentsHomeView: React.FC = () => { <> } + leading={} > {selectedAgentBadge?.label} @@ -863,172 +849,171 @@ const AgentsHomeView: React.FC = () => { > {selectedAgent ? (
- + + + {activeDetailSection === 'basic' ? ( + +
+
+
{t('agentsOverview.detail.name')}
+
{selectedAgent.name}
+
+
+
{t('agentsOverview.detail.source')}
+
{selectedAgentSourceLabel}
+
+
+
{t('agentsOverview.detail.type')}
+
{selectedAgentBadge?.label}
+
+
+
{t('agentsOverview.detail.followUp')}
+
+ {selectedAgent.supportsFollowUp === false + ? t('agentsOverview.detail.unsupported') + : t('agentsOverview.detail.supported')} +
+
+
+
{t('agentsOverview.detail.description')}
+
{getAgentDescription(t, selectedAgent)}
+
+
+
+ ) : null} + + {activeDetailSection === 'behavior' ? ( + +
- - {t('agentsOverview.detail.behaviorContext')} - {selectedAgent.capabilities.length} - -
- {selectedAgentCapabilityTabs.map((tab) => { - const tabIcon = tab.icon; - const isActive = activeDetailSection === tab.key; - return ( - - ); - })} - - - - {activeDetailSection === 'basic' ? ( -
-
-
-

{t('agentsOverview.detail.basicInfo')}

-

{t('agentsOverview.detail.basicInfoHint')}

-
-
-
-
- {t('agentsOverview.detail.name')} - {selectedAgent.name} -
-
- {t('agentsOverview.detail.source')} - {selectedAgentSourceLabel} -
-
- {t('agentsOverview.detail.type')} - {selectedAgentBadge?.label} -
-
- {t('agentsOverview.detail.followUp')} - - {selectedAgent.supportsFollowUp === false - ? t('agentsOverview.detail.unsupported') - : t('agentsOverview.detail.supported')} - -
-
- {t('agentsOverview.detail.description')} -

{getAgentDescription(t, selectedAgent)}

-
+
+
+ {t('agentsOverview.capabilities')} + + {selectedAgent.capabilities.length} +
-
- ) : null} - - {activeDetailSection === 'behavior' ? ( -
-
-
-

{t('agentsOverview.detail.behaviorContext')}

-

{t('agentsOverview.detail.behaviorContextHint')}

+
+
+ {selectedAgent.capabilities.map((cap) => ( +
+ + {getCapabilityLabel(t, cap.category)} + +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ {cap.level}/5
-
-
-
-
- {t('agentsOverview.capabilities')} - - {selectedAgent.capabilities.length} - -
-
-
- {selectedAgent.capabilities.map((cap) => ( -
- - {getCapabilityLabel(t, cap.category)} - -
- {Array.from({ length: 5 }).map((_, i) => ( - ))}
- {cap.level}/5
- ))} -
-
- {selectedAgent.agentKind === 'mode' && selectedAgentUsesSharedProfile ? ( -
-
-
- {t('agentsOverview.sharedProfileLabel')} -
-
-
- - {selectedAgentModeProfile?.profileLabel ?? t('agentsOverview.sharedProfileDefaultLabel')} - -
-

- {t('agentsOverview.sharedProfileDescription', { - modes: selectedAgentProfileMemberNames.join(', '), - })} -

-
- ) : null} + {isPrimaryAgent(selectedAgent) && selectedAgentUsesSharedProfile ? ( +
+
+
+ {t('agentsOverview.sharedProfileLabel')} +
+
+
+ + {selectedAgentModeProfile?.profileLabel ?? t('agentsOverview.sharedProfileDefaultLabel')} + +
+

+ {t('agentsOverview.sharedProfileDescription', { + modes: selectedAgentProfileMemberNames.join(', '), + })} +

) : null} + + ) : null} - {selectedAgentCapabilityTabs.some((tab) => tab.key === activeDetailSection) ? ( -
-
-
- {currentCapabilityMeta ? : null} - {currentCapabilityMeta?.label} - {currentCapabilityMeta?.count ? ( - {currentCapabilityMeta.count} - ) : null} -
- {canManageCurrentCapability ? ( + {selectedAgentCapabilityTabs.some((tab) => tab.key === activeDetailSection) ? ( + + {currentCapabilityMeta?.label} + {currentCapabilityMeta?.count ? ( + {currentCapabilityMeta.count} + ) : null} + + )} + actions={canManageCurrentCapability ? (
{isCurrentTabEditing ? ( <> + currentCapabilityTab === 'tools' + ? t('agentsOverview.toolsReset') + : currentCapabilityTab === 'skills' + ? t('agentsOverview.reset') + : t('agentsOverview.reset') + }> { return; } if (currentCapabilityTab === 'skills') { - await handleResetSkills(selectedAgent.id); - setSkillsEditing(false); - setPendingSkills(null); + setSavingSkills(true); + try { + if (await handleResetSkills(selectedAgent.id) !== false) { + setSkillsEditing(false); + setPendingSkills(null); + } + } finally { + setSavingSkills(false); + } return; } setSavingSubagents(true); @@ -1134,11 +1125,12 @@ const AgentsHomeView: React.FC = () => { } setSavingSkills(true); try { - await handleSetSkills(selectedAgent.id, pendingSkills); + if (await handleSetSkills(selectedAgent.id, pendingSkills) !== false) { + setSkillsEditing(false); + setPendingSkills(null); + } } finally { setSavingSkills(false); - setSkillsEditing(false); - setPendingSkills(null); } return; } @@ -1198,130 +1190,90 @@ const AgentsHomeView: React.FC = () => { )}
- ) : null} -
- - {currentCapabilityTab === 'model' - && selectedAgent.agentKind === 'subagent' - && !selectedAgentIsExternal ? ( - void handleSubagentModelChange(value)} - disabled={savingSubagentModel} - data-testid="agent-detail-subagent-model-select" - /> - ) : null} + ) : undefined} + > - {currentCapabilityTab === 'tools' ? ( - toolCatalogMessage ? ( - - {toolCatalogMessage} - - ) : selectedAgent.agentKind === 'mode' && toolsEditing ? ( - - ) : ( - void handleSubagentModelChange(value)} + disabled={savingSubagentModel} + data-testid="agent-detail-subagent-model-select" /> - ) + ) : null} + + {currentCapabilityTab === 'tools' ? ( + toolCatalogMessage ? ( + + {toolCatalogMessage} + + ) : isPrimaryAgent(selectedAgent) && toolsEditing ? ( + + ) : ( + + ) + ) : null} + + {currentCapabilityTab === 'skills' && skillGroupsError ? ( +
+ {t('agentsOverview.skillGroupPicker.loadFailed')} + +
) : null} {currentCapabilityTab === 'skills' - && selectedAgentHasSkillTool - && selectedAgentSkillConfigs.length > 0 ? ( - skillsEditing ? ( - - ) : ( - - ) - ) : null} + && selectedAgentHasSkillTool + && selectedAgentSkillConfigs.length > 0 ? ( + skillsEditing ? ( + + ) : ( + + ) + ) : null} - {currentCapabilityTab === 'subagents' - && selectedAgent.agentKind === 'mode' - && selectedAgentHasTaskTool ? ( - selectedAgentManageableSubagents.length === 0 ? ( - - {t('agentsOverview.noSubagents')} - - ) : subagentsEditing ? ( -
- {selectedAgentManageableSubagents.map((subagent: SubagentInfo) => { - const isOn = (pendingSubagentIds ?? selectedAgentEnabledSubagentIds).includes(subagent.id); - const isExternal = !isLocallyManageableSubagent(subagent); - const tooltipFields = subagentTooltipFields(subagent, t, isExternal); - return ( - - - - - - ); - })} -
- ) : ( -
- {selectedAgentEnabledSubagents.length === 0 ? ( - - {t('agentsOverview.noSubagents')} - - ) : ( - selectedAgentEnabledSubagents.map((subagent: SubagentInfo) => { - const tooltipFields = subagentTooltipFields( - subagent, - t, - !isLocallyManageableSubagent(subagent), - ); + {currentCapabilityTab === 'subagents' + && isPrimaryAgent(selectedAgent) + && selectedAgentHasTaskTool ? ( + selectedAgentManageableSubagents.length === 0 ? ( + + {t('agentsOverview.noSubagents')} + + ) : subagentsEditing ? ( +
+ {selectedAgentManageableSubagents.map((subagent: SubagentInfo) => { + const isOn = (pendingSubagentIds ?? selectedAgentEnabledSubagentIds).includes(subagent.id); + const isExternal = !isLocallyManageableSubagent(subagent); + const tooltipFields = subagentTooltipFields(subagent, t, isExternal); return ( { description={subagent.description} fields={tooltipFields} > - {subagent.name} + + + ); - }) - )} -
- ) - ) : null} -
- ) : null} - -
+ })} +
+ ) : ( +
+ {selectedAgentEnabledSubagents.length === 0 ? ( + + {t('agentsOverview.noSubagents')} + + ) : ( + selectedAgentEnabledSubagents.map((subagent: SubagentInfo) => { + const tooltipFields = subagentTooltipFields( + subagent, + t, + !isLocallyManageableSubagent(subagent), + ); + return ( + + {subagent.name} + + ); + }) + )} +
+ ) + ) : null} + + ) : null} + +
) : null} diff --git a/src/web-ui/src/app/scenes/agents/agentAppearance.ts b/src/web-ui/src/app/scenes/agents/agentAppearance.ts index 49d48d04c8..54d0540a8d 100644 --- a/src/web-ui/src/app/scenes/agents/agentAppearance.ts +++ b/src/web-ui/src/app/scenes/agents/agentAppearance.ts @@ -32,7 +32,7 @@ export function getCapabilityAccentBorder(category: CapabilityCategory): string } export const CORE_AGENT_ACCENTS = { - agentic: { + Standard: { accentColor: 'var(--openbitfun-color-accent-default)', accentBg: getAlphaColor('var(--openbitfun-color-accent-default)', '1a', 10), }, @@ -46,7 +46,7 @@ export const CORE_AGENT_ACCENTS = { }, } as const satisfies Record; -export const DEFAULT_CORE_AGENT_ACCENT: AgentAccentStyle = CORE_AGENT_ACCENTS.agentic; +export const DEFAULT_CORE_AGENT_ACCENT: AgentAccentStyle = CORE_AGENT_ACCENTS.Standard; export const AGENT_TEAM_TAG_COLORS = [ { @@ -58,7 +58,7 @@ export const AGENT_TEAM_TAG_COLORS = [ border: getAlphaColor(CORE_AGENT_ACCENTS.Cowork.accentColor), }, { - color: CORE_AGENT_ACCENTS.agentic.accentColor, - border: getAlphaColor(CORE_AGENT_ACCENTS.agentic.accentColor), + color: CORE_AGENT_ACCENTS.Standard.accentColor, + border: getAlphaColor(CORE_AGENT_ACCENTS.Standard.accentColor), }, ] as const; diff --git a/src/web-ui/src/app/scenes/agents/agentVisibility.ts b/src/web-ui/src/app/scenes/agents/agentVisibility.ts index 1a5562a6c9..c17e7bbe71 100644 --- a/src/web-ui/src/app/scenes/agents/agentVisibility.ts +++ b/src/web-ui/src/app/scenes/agents/agentVisibility.ts @@ -1,7 +1,5 @@ /** Agent IDs hidden from the Agents overview UI (not listed, not counted). */ -export const STATIC_HIDDEN_AGENT_IDS = new Set([ - 'Claw', -]); +export const STATIC_HIDDEN_AGENT_IDS = new Set(); export const FALLBACK_REVIEW_HIDDEN_AGENT_IDS = new Set([ 'DeepReview', @@ -20,15 +18,21 @@ export const HIDDEN_AGENT_IDS = new Set([ ...FALLBACK_REVIEW_HIDDEN_AGENT_IDS, ]); -/** Core mode agents shown in the top zone only; excluded from overview zone list and counts. */ -export const CORE_AGENT_IDS = new Set(['agentic', 'Cowork', 'ComputerUse']); +/** Runtime invocation role is independent of source and presentation. */ +export function isPrimaryAgent(agent: T | null | undefined): agent is T & { agentKind: 'harness' | 'agent' } { + return agent?.agentKind === 'harness' || agent?.agentKind === 'agent'; +} + +export function isOrdinaryAgent(agent: { agentKind?: string }): boolean { + return agent.agentKind === 'agent'; +} /** Agents that appear in the bottom overview grid (same pool as filter chip counts). */ export function isAgentInOverviewZone( - agent: { id: string }, + agent: { id: string; agentKind?: string }, hiddenAgentIds: ReadonlySet = HIDDEN_AGENT_IDS, ): boolean { - return !hiddenAgentIds.has(agent.id) && !CORE_AGENT_IDS.has(agent.id); + return !hiddenAgentIds.has(agent.id) && agent.agentKind !== 'harness' && !isOrdinaryAgent(agent); } /** External subagents are visible in the overview but managed by their source adapter. */ diff --git a/src/web-ui/src/app/scenes/agents/agentsStore.ts b/src/web-ui/src/app/scenes/agents/agentsStore.ts index 7f11ef59ef..bcf4096b3e 100644 --- a/src/web-ui/src/app/scenes/agents/agentsStore.ts +++ b/src/web-ui/src/app/scenes/agents/agentsStore.ts @@ -3,6 +3,7 @@ */ import { create } from 'zustand'; import type { SubagentInfo } from '@/infrastructure/api/service-api/SubagentAPI'; +import type { ModeInfo } from '@/infrastructure/api/service-api/AgentAPI'; import type { SubagentModelSelection } from '@/infrastructure/config/types'; import { CAPABILITY_ACCENT, @@ -13,32 +14,39 @@ import { export { CAPABILITY_CATEGORIES }; export type { CapabilityCategory }; -/** 'mode' = primary agent mode (e.g. Agentic/Cowork); 'subagent' = sub-agent */ -export type AgentKind = 'mode' | 'subagent'; +export type { AgentKind } from '@/shared/agents/identity'; export interface AgentCapability { category: CapabilityCategory; level: number; } -export interface AgentWithCapabilities extends SubagentInfo { +type PrimaryAgentEntry = ModeInfo & { + key: string; + agentKind: 'harness' | 'agent'; + subagentSource?: never; + visibility?: never; + externalProviderLabel?: never; + supportsFollowUp?: never; +}; + +/** Invocation roles retain their own metadata instead of fabricating SubagentInfo for a Harness. */ +export type AgentWithCapabilities = (PrimaryAgentEntry | (SubagentInfo & { agentKind: 'subagent' })) & { capabilities: AgentCapability[]; iconKey?: string; - /** Distinguishes primary agent mode from sub-agent */ - agentKind?: AgentKind; visibleSubagentCount?: number; /** Explicit model selection for this Subagent, if it overrides the shared default. */ subagentModelOverride?: SubagentModelSelection; /** Display name for an explicitly configured Subagent model override. */ subagentModelDisplayName?: string; -} +}; export const CAPABILITY_COLORS: Record = CAPABILITY_ACCENT; export type AgentsScenePage = 'home' | 'createAgent'; export type AgentEditorMode = 'create' | 'edit'; export type AgentFilterLevel = 'all' | 'builtin' | 'user' | 'project' | 'external'; -export type AgentFilterType = 'all' | 'mode' | 'subagent'; +export type AgentFilterType = 'all' | 'agent' | 'subagent'; interface AgentsStoreState { page: AgentsScenePage; diff --git a/src/web-ui/src/app/scenes/agents/components/AgentCard.appearance.ts b/src/web-ui/src/app/scenes/agents/components/AgentCard.appearance.ts index b1172568b9..1fb87a8c6f 100644 --- a/src/web-ui/src/app/scenes/agents/components/AgentCard.appearance.ts +++ b/src/web-ui/src/app/scenes/agents/components/AgentCard.appearance.ts @@ -1,2 +1,2 @@ import type { AppearanceSurfaceDescriptor } from '@/infrastructure/appearance'; -export const agentCardAppearanceDescriptor: AppearanceSurfaceDescriptor = { id: 'agent-card', parts: [{ id: 'root' }, { id: 'header' }, { id: 'iconArea' }, { id: 'icon' }, { id: 'headerInfo' }, { id: 'titleRow' }, { id: 'name' }, { id: 'badges' }, { id: 'body' }, { id: 'description' }, { id: 'footer' }, { id: 'capabilities' }, { id: 'meta' }] }; +export const agentCardAppearanceDescriptor: AppearanceSurfaceDescriptor = { id: 'agent-card', componentAttribute: 'data-openbitfun-product-component', parts: [{ id: 'root' }, { id: 'header' }, { id: 'iconArea' }, { id: 'icon' }, { id: 'headerInfo' }, { id: 'titleRow' }, { id: 'name' }, { id: 'badges' }, { id: 'body' }, { id: 'description' }, { id: 'footer' }, { id: 'capabilities' }, { id: 'meta' }] }; diff --git a/src/web-ui/src/app/scenes/agents/components/AgentCard.scss b/src/web-ui/src/app/scenes/agents/components/AgentCard.scss index f9dc7cffdd..be58269016 100644 --- a/src/web-ui/src/app/scenes/agents/components/AgentCard.scss +++ b/src/web-ui/src/app/scenes/agents/components/AgentCard.scss @@ -1,352 +1,98 @@ -@use '../../../styles/surface-stagger'; -@use './AgentSurfaceCard' as surface; - -.agent-card { - width: 100%; - min-width: 0; - height: 148px; - border: 0; - border-radius: var(--openbitfun-layout-field-group-radius); - background: var(--openbitfun-color-surface-tertiary); - display: grid; - grid-template-columns: 56px minmax(0, 1fr); - position: relative; - overflow: hidden; - cursor: pointer; - container-name: agent-card; - container-type: inline-size; - animation: agent-card-in 0.22s var(--openbitfun-motion-easing-decelerate) both; - @include surface-stagger.animation-delay(35ms); - transition: - transform var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-decelerate), - border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - background-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - &:hover { - border-color: var(--openbitfun-color-field-border-hover); - background: var(--openbitfun-color-surface-subtle); - } - - &:focus-visible { - outline: var(--openbitfun-focus-width) solid var(--openbitfun-color-focus-ring); - outline-offset: 2px; - } - - &__icon-area { - grid-column: 1; - grid-row: 1; - display: flex; - align-items: flex-start; - justify-content: center; - padding: 12px 8px; - position: relative; - z-index: 1; - - &::after { - content: ""; - position: absolute; - inset-block: 12px; - inset-inline-end: 0; - width: 1px; - background: var(--openbitfun-color-border-subtle); +// Agent detail composition. Shared controls own their interaction and paint. +.agent-detail-modal { + [data-openbitfun-component='gallery-detail-modal'] { + &[data-openbitfun-part='root'] { + gap: var(--openbitfun-space-4); + padding: 0; } - } - - &__icon-tile { - display: flex; - align-items: center; - justify-content: center; - width: 38px; - height: 38px; - border-radius: var(--openbitfun-layout-field-group-radius); - background: var(--openbitfun-color-surface-subtle); - position: relative; - z-index: 1; - } - - &__dot-field { - @include surface.agent-icon-dot-field(); - } - - &__icon { - color: var(--openbitfun-color-content-primary); - } - - &__content { - grid-column: 2; - min-width: 0; - display: flex; - flex-direction: column; - gap: 6px; - padding: 10px; - position: relative; - z-index: 1; - } - - &__header { - min-width: 0; - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--openbitfun-space-2); - } - - &__header-info { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 4px; - overflow: hidden; - } - - &__title-row { - display: flex; - align-items: center; - width: 100%; - min-width: 0; - } - - &__name { - font-size: var(--openbitfun-type-label-md-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - color: var(--openbitfun-color-content-primary); - line-height: var(--openbitfun-type-body-sm-line-height); - white-space: nowrap; - overflow: hidden; - } - - &__badges { - display: flex; - gap: 4px; - flex-wrap: wrap; - flex-shrink: 0; - } - &__kind-badge { - max-width: 112px; - padding: 1px 6px; - overflow: hidden; - border-color: transparent; - background: var(--openbitfun-color-surface-subtle); - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-micro-font-size); - } - - &__body { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - overflow: hidden; - } + &[data-openbitfun-part='hero'] { + align-items: flex-start; + gap: var(--openbitfun-space-3); + padding-inline-end: calc(var(--openbitfun-control-height-sm) + var(--openbitfun-space-2)); + } - &__desc { - margin: 0; - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-body-sm-font-weight); - color: var(--openbitfun-color-content-secondary); - line-height: var(--openbitfun-type-body-lg-line-height); - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - word-break: break-word; - } + &[data-openbitfun-part='icon'] { + width: var(--openbitfun-control-height-lg); + height: var(--openbitfun-control-height-lg); + border-radius: var(--openbitfun-radius-base); + background: var(--openbitfun-color-surface-subtle); + box-shadow: none; + } - &__meta { - height: 100%; - display: grid; - grid-auto-flow: column; - grid-auto-columns: minmax(0, 1fr); - align-items: stretch; - } + &[data-openbitfun-part='summary'] { + gap: var(--openbitfun-space-2); + } - &__meta-item { - min-width: 0; - display: flex; - align-items: center; - justify-content: center; - gap: 4px; - padding-inline: 5px; - overflow: hidden; + &[data-openbitfun-part='title'] { + font-family: var(--openbitfun-type-heading-dialog-font-family); + font-size: var(--openbitfun-type-heading-dialog-font-size); + font-weight: var(--openbitfun-type-heading-dialog-font-weight); + line-height: var(--openbitfun-type-heading-dialog-line-height); + letter-spacing: var(--openbitfun-type-heading-dialog-letter-spacing); + overflow-wrap: anywhere; + } - strong { - min-width: 0; + &[data-openbitfun-part='description'] { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; overflow: hidden; - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-meta-font-size); - font-variant-numeric: tabular-nums; - line-height: var(--openbitfun-type-modifier-leading-none-line-height); - white-space: nowrap; - - &.agent-card__meta-value--text { - font-size: var(--openbitfun-type-overline-sm-font-size); - } + overflow-wrap: anywhere; } - } - - &__meta-icon { - width: 20px; - height: 20px; - display: inline-grid; - place-items: center; - flex: none; - border-radius: 5px; - background: var(--openbitfun-color-surface-tertiary); - color: var(--openbitfun-color-content-secondary); - } - &__meta-label { - overflow: hidden; - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-overline-sm-font-size); - line-height: var(--openbitfun-type-modifier-leading-none-line-height); - white-space: nowrap; - } + &[data-openbitfun-part='meta'] { + gap: var(--openbitfun-space-3); - &__cap-chips { - display: inline-flex; - align-items: center; - gap: 3px; - flex-wrap: nowrap; - min-width: 0; - max-width: 100%; - overflow: hidden; - } - - &__cap-chip { - display: inline-flex; - align-items: center; - padding: 1px 6px; - border-radius: var(--openbitfun-radius-pill); - border: 1px solid transparent; - background: var(--openbitfun-color-surface-subtle); - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-micro-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); - white-space: nowrap; - } - - &__footer { - margin-top: auto; - padding: 3px; - @include surface.agent-surface-footer(); - } - - &__meta { - position: relative; - z-index: 1; - } -} - -// ── Responsive ── -@media (max-width: 720px) { - .agent-card { - width: 100%; - min-height: 148px; - } -} + > span { + min-height: 0; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + color: inherit; + } + } -@container agent-card (max-width: 330px) { - .agent-card { - &__content { - padding-inline: 9px; + &[data-openbitfun-part='heroActions'] { + padding-inline-end: 0; } - &__meta-label, - &__cap-chip:nth-child(n + 2) { - display: none; + &[data-openbitfun-part='content'] { + padding-top: var(--openbitfun-space-4); } } } -// ───────────────────────────────────────────────────────────────────────────── -// Detail modal content styles (rendered inside GalleryDetailModal) -// These classes are used in AgentsScene.tsx for the agent detail popup -// ───────────────────────────────────────────────────────────────────────────── - .agent-card { - // ── Detail information architecture ───────────────────────────────────── - &__config-panel { - padding: 14px 16px; - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-layout-field-group-radius); - background: var(--openbitfun-color-surface-tertiary); - } - &__configuration { display: grid; grid-template-columns: 150px minmax(0, 1fr); align-items: stretch; flex: 1 1 auto; min-height: 0; - gap: 14px; + gap: var(--openbitfun-space-4); } &__config-nav { display: flex; flex-direction: column; - gap: 4px; - position: sticky; - inset-block-start: 0; + align-self: start; + gap: var(--openbitfun-space-1); + min-width: 0; + max-height: 100%; + overflow-y: auto; + padding: var(--openbitfun-space-1); } &__config-nav-item { - width: 100%; + flex: 0 0 auto; min-width: 0; - min-height: 34px; - display: grid; - grid-template-columns: 18px minmax(0, 1fr) auto; - align-items: center; - gap: 7px; - padding: 0 10px; - border: 0; - border-radius: var(--openbitfun-radius-base); - background: transparent; - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-meta-font-size); - text-align: start; - cursor: pointer; - transition: - background-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - > span:not([data-overflow-content]):not(.agent-card__config-nav-count) { - overflow: hidden; - white-space: nowrap; - } - - &:hover { - background: var(--openbitfun-color-surface-subtle); - color: var(--openbitfun-color-content-primary); - } - - &.is-active { - background: var(--openbitfun-color-action-neutral-surface-hover); - color: var(--openbitfun-color-content-primary); - font-weight: var(--openbitfun-type-label-selected-font-weight); - } - } - - &__config-nav-count { - min-width: 20px; - padding: 1px 5px; - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-pill); - background: var(--openbitfun-color-surface-tertiary); - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-overline-sm-font-size); font-variant-numeric: tabular-nums; - text-align: center; } &__config-nav-divider { - height: 1px; - margin: 4px 8px; - background: var(--openbitfun-color-border-subtle); + margin-block: var(--openbitfun-space-2); } &__config-main { @@ -355,377 +101,157 @@ min-width: 0; min-height: 0; height: 100%; - padding-inline-end: 2px; + padding-inline: var(--openbitfun-space-4) var(--openbitfun-space-2); + border-inline-start: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); + scrollbar-gutter: stable; - > .agent-card__config-panel, - > .agent-card__section { + > .agent-card__config-panel { flex: 1 0 auto; min-height: 100%; } } &__config-panel { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-3); - } - - &__config-panel-head { - padding-bottom: 10px; - border-bottom: 1px solid var(--openbitfun-color-border-subtle); - - h3, - p { - margin: 0; - } + gap: var(--openbitfun-space-4); + padding-block: var(--openbitfun-space-1) var(--openbitfun-space-2); - h3 { - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-label-md-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - } - - p { - margin-top: 3px; - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-micro-font-size); - line-height: var(--openbitfun-type-body-lg-line-height); + > [data-openbitfun-part='header'] { + flex-wrap: wrap; } } &__field-grid { display: grid; - flex: 1 1 auto; grid-template-columns: repeat(2, minmax(0, 1fr)); - grid-template-rows: repeat(2, minmax(52px, auto)) minmax(76px, 1fr); - gap: 10px; + align-content: start; + gap: var(--openbitfun-space-3) var(--openbitfun-space-6); + margin: 0; } &__field { + display: flex; + flex-direction: column; min-width: 0; - min-height: 52px; - padding: 8px 10px; - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-base); - background: var(--openbitfun-color-surface-subtle); - - > span:not([data-overflow-content]), - > strong { - display: block; - overflow: hidden; - white-space: nowrap; - } + gap: var(--openbitfun-space-1); + padding-block: var(--openbitfun-space-2) var(--openbitfun-space-3); + border-bottom: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); - > span { + > dt { color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-overline-sm-font-size); + font-size: var(--openbitfun-type-label-sm-font-size); + line-height: var(--openbitfun-type-label-sm-line-height); } - > strong, - > p { - margin: 5px 0 0; + > dd { + min-width: 0; + margin: 0; color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-meta-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); - line-height: var(--openbitfun-type-body-lg-line-height); + font-size: var(--openbitfun-type-body-sm-font-size); + line-height: var(--openbitfun-type-body-sm-line-height); + overflow-wrap: anywhere; } &--wide { grid-column: 1 / -1; - min-height: 76px; + border-bottom: 0; } } - // ── Capability grid ────────────────────────────────────────────────────── &__cap-grid { display: flex; flex-direction: column; - gap: var(--openbitfun-space-2); + gap: var(--openbitfun-space-3); } &__cap-row { display: flex; align-items: center; - gap: var(--openbitfun-space-2); + gap: var(--openbitfun-space-3); } &__cap-label { - flex-shrink: 0; - width: 72px; + flex: 0 0 72px; + color: var(--openbitfun-color-content-secondary); font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); - white-space: nowrap; - overflow: hidden; + line-height: var(--openbitfun-type-label-sm-line-height); } &__cap-bar { flex: 1; display: flex; align-items: center; - gap: 3px; + gap: var(--openbitfun-space-1); } &__cap-pip { width: 100%; - height: 5px; - border-radius: 2px; + height: var(--openbitfun-space-1); + border-radius: var(--openbitfun-radius-pill); background: var(--openbitfun-color-action-neutral-surface-hover); - transition: background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); } &__cap-level { - flex-shrink: 0; - width: 28px; - text-align: right; - font-size: var(--openbitfun-type-label-sm-font-size); + flex: 0 0 28px; + text-align: end; + font-size: var(--openbitfun-type-meta-font-size); color: var(--openbitfun-color-content-muted); font-variant-numeric: tabular-nums; } - // ── Content sections ───────────────────────────────────────────────────── &__section { display: flex; flex-direction: column; - gap: var(--openbitfun-space-2); - padding: 10px 12px; - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-layout-field-group-radius); - background: var(--openbitfun-color-surface-tertiary); + gap: var(--openbitfun-space-3); - &--capabilities { - background: var(--openbitfun-color-surface-subtle); + + .agent-card__section { + padding-top: var(--openbitfun-space-4); + border-top: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); } } - &__section-head { + &__section-head, + &__section-title, + &__section-actions { display: flex; align-items: center; - justify-content: space-between; gap: var(--openbitfun-space-2); - min-height: 24px; - - &--tabs { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: center; - } + min-width: 0; } &__section-title { - display: inline-flex; - align-items: center; - gap: var(--openbitfun-space-2); + flex-wrap: wrap; + } + + &__section-head &__section-title { font-size: var(--openbitfun-type-label-md-font-size); font-weight: var(--openbitfun-type-label-selected-font-weight); - color: var(--openbitfun-color-content-primary); + line-height: var(--openbitfun-type-label-md-line-height); + color: var(--openbitfun-color-content-secondary); } &__section-count { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 1px 6px; - border-radius: var(--openbitfun-radius-pill); - min-width: 20px; - background: var(--openbitfun-color-surface-tertiary); - font-size: var(--openbitfun-type-micro-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); + font-size: var(--openbitfun-type-meta-font-size); + font-weight: var(--openbitfun-type-meta-font-weight); color: var(--openbitfun-color-content-muted); font-variant-numeric: tabular-nums; - text-transform: none; - letter-spacing: var(--openbitfun-type-body-sm-letter-spacing); } &__section-actions { - display: flex; - align-items: center; - gap: var(--openbitfun-space-2); - flex-shrink: 0; + flex-wrap: wrap; } &__section-note { margin: 0; - font-size: var(--openbitfun-type-label-sm-font-size); + font-size: var(--openbitfun-type-body-sm-font-size); color: var(--openbitfun-color-content-secondary); line-height: var(--openbitfun-type-body-sm-line-height); } - &__tab-list { - flex: 1 1 auto; - display: flex; - align-items: center; - gap: 4px; - flex-wrap: wrap; - min-width: 0; - padding: 3px; - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-base); - background: var(--openbitfun-color-surface-subtle); - } - - &__tab { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 5px; - flex: 1 1 0; - min-width: 0; - min-height: 30px; - padding: 0 7px; - border: 1px solid transparent; - border-radius: 6px; - background: transparent; - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-meta-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - letter-spacing: var(--openbitfun-type-modifier-tracking-wide-letter-spacing); - white-space: nowrap; - cursor: pointer; - transition: - border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - > span:not([data-overflow-content]):not(.agent-card__tab-count) { - min-width: 0; - overflow: hidden; - } - - &:hover { - background: var(--openbitfun-color-action-neutral-surface-hover); - color: var(--openbitfun-color-content-secondary); - } - - &.is-active { - border-color: var(--openbitfun-color-border-subtle); - background: var(--openbitfun-color-surface-tertiary); - color: var(--openbitfun-color-content-primary); - } - } - - &__tab-count { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 20px; - padding: 0 5px; - border-radius: 999px; - background: var(--openbitfun-color-action-neutral-surface-hover); - color: inherit; - font-size: var(--openbitfun-type-overline-sm-font-size); - line-height: var(--openbitfun-type-modifier-leading-ui-line-height); - font-weight: var(--openbitfun-type-label-selected-font-weight); - font-variant-numeric: tabular-nums; - border: 1px solid var(--openbitfun-color-border-subtle); - } - - &__skill-groups { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-2); - } - - &__skill-group { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-1); - padding: var(--openbitfun-space-2); - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-base); - background: var(--openbitfun-color-surface-subtle); - } - - &__skill-group-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--openbitfun-space-2); - min-height: 26px; - } - - &__skill-group-title-wrap { - display: inline-flex; - align-items: center; - gap: var(--openbitfun-space-2); - min-width: 0; - } - - &__skill-group-title { - font-size: var(--openbitfun-type-meta-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - color: var(--openbitfun-color-content-secondary); - text-transform: uppercase; - letter-spacing: var(--openbitfun-type-modifier-tracking-wide-letter-spacing); - } - - &__skill-group-count { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 22px; - padding: 1px 6px; - border-radius: var(--openbitfun-radius-pill); - background: var(--openbitfun-color-action-neutral-surface-hover); - font-size: var(--openbitfun-type-micro-font-size); - color: var(--openbitfun-color-content-muted); - font-variant-numeric: tabular-nums; - } - - &__skill-group-actions { - display: flex; - align-items: center; - gap: var(--openbitfun-space-1); - flex-wrap: wrap; - justify-content: flex-end; - } - - // ── Read-only chip grid ────────────────────────────────────────────────── - &__chip-grid { - display: flex; - flex-wrap: wrap; - gap: var(--openbitfun-space-1); - } - - &__chip { - display: inline-flex; - align-items: center; - padding: 3px 9px; - border-radius: var(--openbitfun-radius-pill); - border: 1px solid var(--openbitfun-color-border-subtle); - background: var(--openbitfun-color-surface-subtle); - font-size: var(--openbitfun-type-meta-font-size); - color: var(--openbitfun-color-content-secondary); - white-space: nowrap; - max-width: 320px; - overflow: hidden; - - &.is-covered { - border-color: color-mix(in srgb, var(--openbitfun-color-status-warning-content) 32%, var(--openbitfun-color-border-subtle)); - color: var(--openbitfun-color-content-muted); - } - } - - &__chip-state, - &__token-state { - flex-shrink: 0; - margin-left: var(--openbitfun-space-1); - font-family: var(--openbitfun-type-body-sm-font-family); - font-size: var(--openbitfun-type-micro-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); - color: var(--openbitfun-color-content-muted); - } - - &__chip.is-covered &__chip-state, - &__token.is-covered &__token-state { - color: var(--openbitfun-color-status-warning-content); - } - - // ── Editable token grid ────────────────────────────────────────────────── + &__chip-grid, &__token-grid { display: flex; flex-wrap: wrap; - gap: var(--openbitfun-space-1); + gap: var(--openbitfun-space-2); } &__tooltip-trigger { @@ -734,71 +260,13 @@ } &__token { - display: inline-flex; - align-items: center; - padding: 3px 9px; - border-radius: var(--openbitfun-radius-pill); - border: 1px solid transparent; - background: transparent; - font-size: var(--openbitfun-type-meta-font-size); - font-family: var(--openbitfun-type-code-md-font-family); - color: var(--openbitfun-color-content-muted); - cursor: pointer; - transition: - border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - &:hover:not(.is-on) { - border-color: var(--openbitfun-color-field-border-hover); - color: var(--openbitfun-color-content-secondary); - } - - &.is-on { - border: 1px solid color-mix(in srgb, var(--openbitfun-color-accent-default) 40%, transparent); - background: var(--openbitfun-color-action-secondary-background); - color: var(--openbitfun-color-accent-default); - - &:hover { - border-color: color-mix(in srgb, var(--openbitfun-color-accent-default) 60%, transparent); - background: color-mix(in srgb, var(--openbitfun-color-accent-default) 12%, transparent); - } - } - - &.is-covered { - border-color: color-mix(in srgb, var(--openbitfun-color-status-warning-content) 32%, var(--openbitfun-color-border-subtle)); - background: color-mix(in srgb, var(--openbitfun-color-status-warning-content) 8%, transparent); - color: var(--openbitfun-color-content-muted); - } - - &.is-readonly, - &:disabled { - cursor: default; - opacity: 0.72; - - &:hover:not(.is-on) { - border-color: transparent; - color: var(--openbitfun-color-content-muted); - } - - &.is-on:hover { - border-color: color-mix(in srgb, var(--openbitfun-color-accent-default) 40%, transparent); - background: var(--openbitfun-color-action-secondary-background); - } - } - } - - &__token-name { - max-width: 160px; - overflow: hidden; - white-space: nowrap; + max-width: min(240px, 100%); } - // ── Empty inline state ─────────────────────────────────────────────────── &__empty-inline { - font-size: var(--openbitfun-type-label-sm-font-size); + font-size: var(--openbitfun-type-body-sm-font-size); + line-height: var(--openbitfun-type-body-sm-line-height); color: var(--openbitfun-color-content-muted); - font-style: italic; } } @@ -807,84 +275,45 @@ &__configuration { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); + gap: var(--openbitfun-space-3); } &__config-nav { - position: static; flex-direction: row; + align-items: center; overflow-x: auto; - padding-bottom: 2px; } &__config-nav-item { - width: auto; - min-width: max-content; + inline-size: max-content; + min-inline-size: max-content; } &__config-nav-divider { - width: 1px; - height: 22px; - margin: 6px 3px; - flex: none; - } - } -} - -@container gallery-detail-modal (max-width: 460px) { - .agent-card { - &__field-grid { - flex: 0 0 auto; - grid-template-columns: minmax(0, 1fr); - grid-template-rows: none; + inline-size: var(--openbitfun-border-width-default); + block-size: var(--openbitfun-space-4); + margin: 0 var(--openbitfun-space-1); } - &__field--wide { - grid-column: auto; - } - - &__section-head--tabs { - grid-template-columns: minmax(0, 1fr); - } - - &__section-head--tabs &__section-actions { - width: 100%; - justify-content: flex-end; - } - - &__tab { - flex-basis: calc(50% - 2px); - } - } -} - -@container gallery-detail-modal (max-width: 360px) { - .agent-card { - &__cap-label { - width: 60px; - } - - &__section { - padding-inline: 10px; + &__config-main { + padding-inline-start: var(--openbitfun-space-1); + border-inline-start: 0; } } -} -// ── Animations ── -@keyframes agent-card-in { - from { - opacity: 0; - transform: translateY(10px) scale(0.98); + .agent-detail-modal [data-openbitfun-component='gallery-detail-modal'][data-openbitfun-part='hero'] { + flex-wrap: wrap; } - to { - opacity: 1; - transform: translateY(0) scale(1); + .agent-detail-modal [data-openbitfun-component='gallery-detail-modal'][data-openbitfun-part='heroActions'] { + width: 100%; + justify-content: flex-start; } } -@media (prefers-reduced-motion: reduce) { - .agent-card { - animation: none; - transition: none; +@container gallery-detail-modal (max-width: 460px) { + .agent-card__field-grid { + grid-template-columns: minmax(0, 1fr); + gap: var(--openbitfun-space-2); } } diff --git a/src/web-ui/src/app/scenes/agents/components/AgentCard.tsx b/src/web-ui/src/app/scenes/agents/components/AgentCard.tsx index f4d4885171..7d6f3e286b 100644 --- a/src/web-ui/src/app/scenes/agents/components/AgentCard.tsx +++ b/src/web-ui/src/app/scenes/agents/components/AgentCard.tsx @@ -1,137 +1,77 @@ import React from 'react'; -import { Bot, Wrench, Cpu, UsersRound } from 'lucide-react'; +import { Bot } from 'lucide-react'; +import { CardBody, CardFooter, CardHeader, Icon, OverflowText } from '@openbitfun/ui'; import { useTranslation } from 'react-i18next'; -import { OverflowText, Icon, StatusPill } from '@openbitfun/ui'; import type { AgentWithCapabilities } from '../agentsStore'; import { AGENT_ICON_MAP } from '../agentsIcons'; import { getAgentBadge, getAgentDescription, getCapabilityLabel } from '../utils'; -import './AgentCard.scss'; +import AgentCatalogCard, { AgentCatalogMetrics } from './AgentCatalogCard'; +import './AgentCatalogCard.scss'; interface AgentCardProps { agent: AgentWithCapabilities; - index?: number; toolCount?: number; skillCount?: number; subagentCount?: number; + disabledReason?: string; onOpenDetails: (agent: AgentWithCapabilities) => void; } -const AgentCard: React.FC = ({ - agent, - index = 0, - toolCount, - skillCount = 0, - subagentCount = 0, - onOpenDetails, -}) => { +const AgentCard: React.FC = ({ agent, disabledReason, onOpenDetails, ...metrics }) => { const { t } = useTranslation('scenes/agents'); const badge = getAgentBadge(t, agent.agentKind, agent.source ?? agent.subagentSource); const agentIcon = AGENT_ICON_MAP[(agent.iconKey ?? 'bot') as keyof typeof AGENT_ICON_MAP] ?? { glyph: Bot }; - const totalTools = toolCount ?? agent.toolCount ?? agent.defaultTools?.length ?? 0; - const capabilityCount = agent.capabilities.length; - const openDetails = () => onOpenDetails(agent); + const capabilities = agent.capabilities.slice(0, 2).map(cap => getCapabilityLabel(t, cap.category)).join(' · '); return ( -
{ - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - openDetails(); - } - }} - aria-label={agent.name} - data-testid="agent-list-item" - data-agent-id={agent.id} - data-agent-name={agent.name} - data-agent-kind={agent.agentKind} - data-subagent-source={agent.subagentSource ?? ''} + -
-
-
- -
-
-
- -
-
-
-
- {agent.name} -
-
- {agent.capabilities.slice(0, 2).map((cap) => ( - - {getCapabilityLabel(t, cap.category)} - - ))} +
- -
- - {getAgentDescription(t, agent)} + )} + description={( + + {badge.label} + {capabilities ? ` · ${capabilities}` : null} -
- -
-
- - - {t('agentCard.metrics.tools')} - {totalTools} - - {agent.agentKind === 'mode' ? ( - <> - - - {t('agentCard.metrics.skills')} - {skillCount} - - - - {t('agentCard.metrics.collaboration')} - {subagentCount} - - - ) : ( - - - {t('agentCard.metrics.capabilities')} - {capabilityCount} - - )} - {agent.agentKind === 'subagent' && agent.subagentModelDisplayName ? ( - - - {t('agentCard.metrics.model')} - - {agent.subagentModelDisplayName} - - - ) : null} -
-
-
-
+ )} + /> + + + {getAgentDescription(t, agent)} + + + + + {disabledReason && ( + + + {disabledReason} + + )} + + ); }; diff --git a/src/web-ui/src/app/scenes/agents/components/AgentCatalogCard.scss b/src/web-ui/src/app/scenes/agents/components/AgentCatalogCard.scss new file mode 100644 index 0000000000..f706ec978d --- /dev/null +++ b/src/web-ui/src/app/scenes/agents/components/AgentCatalogCard.scss @@ -0,0 +1,109 @@ +.agent-catalog-card { + &__surface { + width: 100%; + min-height: 168px; + cursor: pointer; + border-color: var(--openbitfun-color-border-subtle); + background-clip: border-box; + transition: + background-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), + border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); + + &:hover { + background-color: var(--openbitfun-color-surface-subtle); + } + + &:active { + background-color: var(--openbitfun-color-action-neutral-surface-pressed); + } + + &:focus-visible { + outline: var(--openbitfun-focus-width) solid var(--openbitfun-color-focus-ring); + outline-offset: var(--openbitfun-focus-offset); + } + } + + &__header { + gap: var(--openbitfun-space-3); + } + + &__icon { + display: inline-flex; + align-items: center; + min-height: calc(var(--openbitfun-type-heading-card-font-size) * var(--openbitfun-type-heading-card-line-height)); + color: var(--openbitfun-color-content-secondary); + } + + &__title { + width: 100%; + min-width: 0; + } + + &__identity { + color: var(--openbitfun-color-content-muted); + font-size: var(--openbitfun-type-meta-font-size); + line-height: var(--openbitfun-type-meta-line-height); + } + + &__description { + margin: 0; + overflow-wrap: anywhere; + } + + &__footer { + flex-wrap: wrap; + } + + &__metrics { + display: flex; + flex: 1 1 auto; + align-items: center; + flex-wrap: wrap; + gap: var(--openbitfun-space-1) var(--openbitfun-space-3); + min-width: 0; + color: var(--openbitfun-color-content-muted); + font-size: var(--openbitfun-type-meta-font-size); + line-height: var(--openbitfun-type-meta-line-height); + } + + &__metric { + display: inline-flex; + align-items: baseline; + gap: var(--openbitfun-space-1); + } + + &__metric-value { + color: var(--openbitfun-color-content-secondary); + font-variant-numeric: tabular-nums; + } + + &__model { + flex: 1 1 120px; + min-width: 0; + } + + &__status { + display: inline-flex; + align-items: center; + gap: var(--openbitfun-space-1); + max-width: 100%; + color: var(--openbitfun-color-content-muted); + font-size: var(--openbitfun-type-meta-font-size); + line-height: var(--openbitfun-type-meta-line-height); + } + + &__status-icon { + flex: none; + color: var(--openbitfun-color-status-success-content); + } + + &__status[data-openbitfun-state='disabled'] &__status-icon { + color: var(--openbitfun-color-status-warning-content); + } +} + +@media (prefers-reduced-motion: reduce) { + .agent-catalog-card__surface { + transition: none; + } +} diff --git a/src/web-ui/src/app/scenes/agents/components/AgentCatalogCard.tsx b/src/web-ui/src/app/scenes/agents/components/AgentCatalogCard.tsx new file mode 100644 index 0000000000..59ebef55c2 --- /dev/null +++ b/src/web-ui/src/app/scenes/agents/components/AgentCatalogCard.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { Card, OverflowText, type CardProps } from '@openbitfun/ui'; +import { useI18n } from '@/infrastructure/i18n/hooks/useI18n'; +import type { AgentWithCapabilities } from '../agentsStore'; + +interface AgentCatalogCardProps extends Omit { + agent: AgentWithCapabilities; + onOpenDetails: (agent: AgentWithCapabilities) => void; +} + +/** Shared catalog interaction and public Card layout; adapters own identity slots. */ +const AgentCatalogCard: React.FC = ({ agent, onOpenDetails, children, ...props }) => { + const openDetails = () => onOpenDetails(agent); + + return ( + { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + openDetails(); + } + }} + data-testid="agent-list-item" + data-agent-id={agent.id} + data-agent-name={agent.name} + data-agent-kind={agent.agentKind} + data-subagent-source={agent.subagentSource ?? ''} + > + {children} + + ); +}; + +export interface AgentCatalogMetricsProps extends React.HTMLAttributes { + agent: AgentWithCapabilities; + toolCount?: number; + skillCount?: number; + subagentCount?: number; +} + +export const AgentCatalogMetrics: React.FC = ({ + agent, + toolCount, + skillCount = 0, + subagentCount = 0, + ...props +}) => { + const { t } = useI18n('scenes/agents'); + const metrics = [ + { key: 'tools', label: t('agentCard.metrics.tools'), value: toolCount ?? agent.toolCount ?? agent.defaultTools?.length ?? 0 }, + ...(agent.agentKind !== 'subagent' ? [ + { key: 'skills', label: t('agentCard.metrics.skills'), value: skillCount }, + { key: 'collaboration', label: t('agentCard.metrics.collaboration'), value: subagentCount }, + ] : []), + ]; + + return ( +
+ {metrics.map(({ key, label, value }) => ( + + {value} + {label} + + ))} + {agent.agentKind === 'subagent' && agent.subagentModelDisplayName ? ( + + {agent.subagentModelDisplayName} + + ) : null} +
+ ); +}; + +export default AgentCatalogCard; diff --git a/src/web-ui/src/app/scenes/agents/components/AgentHarnessOverview.tsx b/src/web-ui/src/app/scenes/agents/components/AgentHarnessOverview.tsx index 9189621b66..c3bc9223df 100644 --- a/src/web-ui/src/app/scenes/agents/components/AgentHarnessOverview.tsx +++ b/src/web-ui/src/app/scenes/agents/components/AgentHarnessOverview.tsx @@ -1,16 +1,14 @@ +import type { AgentWithCapabilities } from '../agentsStore'; +import { HARNESS_IDS, type HarnessId } from '@/shared/agents/identity'; +import { HARNESS_PRESENTATION } from '@/shared/agents/harnessPresentation'; import React from 'react'; import { Icon } from '@openbitfun/ui'; import { GalleryZone } from '@/app/components'; import { useI18n } from '@/infrastructure/i18n/hooks/useI18n'; -const HARNESS_STRATEGIES = [ - { id: 'minimal', icon: 'minimal', gear: 1 }, - { id: 'balanced', icon: 'standard', gear: 2 }, - { id: 'ultimate', icon: 'ultimate', gear: 3 }, - { id: 'creative', icon: 'creative', gear: 'creative' }, -] as const; +const HARNESS_STRATEGIES = HARNESS_IDS.map(id => ({ id, ...HARNESS_PRESENTATION[id] })); -type HarnessStrategyId = typeof HARNESS_STRATEGIES[number]['id']; +type HarnessStrategyId = HarnessId; // Route coordinates describe execution structure; labels and icons keep their // natural size while the connecting lines adapt to the available width. @@ -18,49 +16,27 @@ const STRATEGY_ROUTES: Record; }> = { - minimal: { + Minimal: { paths: ['M 0 22 H 320'], nodes: [[160, 22]], }, - balanced: { + Standard: { paths: ['M 0 22 H 320'], nodes: [[64, 22], [128, 22], [192, 22], [256, 22]], }, - ultimate: { + Ultimate: { paths: [ 'M 0 22 H 48 C 68 22 70 7 92 7 H 222 C 242 7 245 22 267 22 H 320', 'M 48 22 C 68 22 70 37 92 37 H 222 C 242 37 245 22 267 22', ], nodes: [[48, 22], [116, 7], [174, 7], [124, 37], [192, 37], [267, 22]], }, - creative: { + Creative: { paths: ['M 0 22 C 20 22 24 18 48 18 C 88 18 96 34 144 34 C 184 34 192 10 224 10 C 252 10 256 22 280 22 H 320'], nodes: [[48, 18], [144, 34], [280, 22]], }, }; -function HarnessConnections({ converge = false }: { converge?: boolean }) { - return ( - - ); -} - function HarnessStrategyRoute({ profile }: { profile: HarnessStrategyId }) { const route = STRATEGY_ROUTES[profile]; @@ -80,7 +56,7 @@ function HarnessStrategyRoute({ profile }: { profile: HarnessStrategyId }) { ))} - {profile === 'minimal' && ( + {profile === 'Minimal' && ( @@ -89,7 +65,10 @@ function HarnessStrategyRoute({ profile }: { profile: HarnessStrategyId }) { ); } -const AgentHarnessOverview: React.FC = () => { +const AgentHarnessOverview: React.FC<{ + agents: AgentWithCapabilities[]; + onOpenDetails: (agent: AgentWithCapabilities) => void; +}> = ({ agents, onOpenDetails }) => { const { t } = useI18n('scenes/agents'); return ( @@ -99,53 +78,47 @@ const AgentHarnessOverview: React.FC = () => { className="openbitfun-agents-scene__harness-zone" data-testid="agents-harness-zone" title={t('harnessZone.title')} - subtitle={( - <> - {t('harnessZone.subtitle')} - - {t('harnessZone.flowCaption')} - - - )} + subtitle={t('harnessZone.subtitle')} > -
- - {t('harnessZone.task')} - - -
- {HARNESS_STRATEGIES.map(({ id, icon, gear }) => ( -
{ + const agent = agents.find(candidate => candidate.id === id); + + return ( +
  • - - - {t(`harnessZone.profiles.${id}.name`)} - - - {t(`harnessZone.profiles.${id}.purpose`)} - - -
  • - ))} -
    - - - {t('harnessZone.result')} - -
    + + + ); + })} +
    ); diff --git a/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.appearance.ts b/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.appearance.ts index 0932db6549..73282b82b0 100644 --- a/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.appearance.ts +++ b/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.appearance.ts @@ -2,6 +2,7 @@ import type { AppearanceSurfaceDescriptor } from '@/infrastructure/appearance'; export const coreAgentCardAppearanceDescriptor: AppearanceSurfaceDescriptor = { id: 'core-agent-card', + componentAttribute: 'data-openbitfun-product-component', parts: [ { id: 'root' }, { id: 'header' }, diff --git a/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.scss b/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.scss deleted file mode 100644 index d9e475989c..0000000000 --- a/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.scss +++ /dev/null @@ -1,224 +0,0 @@ -@use './AgentSurfaceCard' as surface; - -.core-agent-card { - @include surface.agent-surface-card(); - - display: grid; - grid-template-columns: 56px minmax(0, 1fr); - container-name: core-agent-card; - container-type: inline-size; - - &__icon-rail { - grid-column: 1; - grid-row: 1; - display: flex; - align-items: flex-start; - justify-content: center; - padding: 12px 8px; - position: relative; - z-index: 1; - - &::after { - content: ""; - position: absolute; - inset-block: 12px; - inset-inline-end: 0; - width: 1px; - background: var(--openbitfun-color-border-subtle); - } - } - - &__icon-wrap { - width: 38px; - height: 38px; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: var(--openbitfun-layout-field-group-radius); - background: var(--openbitfun-color-surface-subtle); - color: var(--openbitfun-color-content-primary); - position: relative; - z-index: 1; - transition: transform var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-decelerate); - } - - &__dot-field { - @include surface.agent-icon-dot-field(); - } - - &__content { - grid-column: 2; - min-width: 0; - display: flex; - flex-direction: column; - gap: 6px; - padding: 10px; - position: relative; - z-index: 1; - } - - &__top { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--openbitfun-space-2); - min-width: 0; - } - - &__top-info { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 4px; - overflow: hidden; - } - - &__name { - font-size: var(--openbitfun-type-label-md-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - color: var(--openbitfun-color-content-primary); - line-height: var(--openbitfun-type-body-sm-line-height); - white-space: nowrap; - overflow: hidden; - } - - &__role { - max-width: 100%; - padding: 1px 6px; - overflow: hidden; - border-color: transparent; - background: var(--openbitfun-color-surface-subtle); - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-micro-font-size); - } - - &__status { - max-width: 46%; - display: inline-flex; - align-items: center; - gap: 4px; - flex: 0 1 auto; - padding-top: 2px; - overflow: hidden; - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-micro-font-size); - line-height: var(--openbitfun-type-body-sm-line-height); - white-space: nowrap; - - .core-agent-card__status-icon { - flex: none; - color: var(--openbitfun-color-status-success-content); - } - - > span:not([data-overflow-content]) { - overflow: hidden; - } - - &.is-disabled { - color: var(--openbitfun-color-content-secondary); - - .core-agent-card__status-icon { - color: var(--openbitfun-color-status-warning-content); - } - } - } - - &__body { - flex: 1; - min-height: 0; - } - - &__desc { - margin: 0; - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-body-sm-font-weight); - color: var(--openbitfun-color-content-secondary); - line-height: var(--openbitfun-type-body-lg-line-height); - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - word-break: break-word; - } - - &__footer { - margin-top: auto; - padding: 3px; - @include surface.agent-surface-footer(); - } - - &__meta { - height: 100%; - display: grid; - grid-auto-flow: column; - grid-auto-columns: minmax(0, 1fr); - align-items: stretch; - position: relative; - z-index: 1; - } - - &__meta-item { - min-width: 0; - display: flex; - align-items: center; - justify-content: center; - gap: 4px; - padding-inline: 5px; - overflow: hidden; - - strong { - min-width: 0; - overflow: hidden; - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-meta-font-size); - font-variant-numeric: tabular-nums; - line-height: var(--openbitfun-type-modifier-leading-none-line-height); - white-space: nowrap; - - &.core-agent-card__meta-value--text { - font-size: var(--openbitfun-type-overline-sm-font-size); - } - } - } - - &__meta-icon { - width: 20px; - height: 20px; - display: inline-grid; - place-items: center; - flex: none; - border-radius: 5px; - background: var(--openbitfun-color-surface-tertiary); - color: var(--openbitfun-color-content-secondary); - } - - &__meta-label { - overflow: hidden; - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-overline-sm-font-size); - line-height: var(--openbitfun-type-modifier-leading-none-line-height); - white-space: nowrap; - } - -} - -@container core-agent-card (max-width: 330px) { - .core-agent-card { - &__content { - padding-inline: 9px; - } - - &__meta-label { - display: none; - } - } -} - -@media (prefers-reduced-motion: reduce) { - .core-agent-card { - animation: none; - transition: none; - } -} diff --git a/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.tsx b/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.tsx index ecc7aef203..a291a6c706 100644 --- a/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.tsx +++ b/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { Bot, Wrench, Cpu, UsersRound } from 'lucide-react'; +import { Bot } from 'lucide-react'; +import { CardBody, CardFooter, CardHeader, Icon, OverflowText } from '@openbitfun/ui'; import { useTranslation } from 'react-i18next'; -import { OverflowText, Icon, StatusPill } from '@openbitfun/ui'; import type { AgentWithCapabilities } from '../agentsStore'; import { AGENT_ICON_MAP } from '../agentsIcons'; import { getAgentDescription } from '../utils'; -import './CoreAgentCard.scss'; +import AgentCatalogCard, { AgentCatalogMetrics } from './AgentCatalogCard'; +import './AgentCatalogCard.scss'; export interface CoreAgentMeta { role: string; @@ -15,122 +16,70 @@ export interface CoreAgentMeta { interface CoreAgentCardProps { agent: AgentWithCapabilities; - index?: number; meta: CoreAgentMeta; toolCount?: number; skillCount?: number; subagentCount?: number; onOpenDetails: (agent: AgentWithCapabilities) => void; - /** Replaces the connected status when the agent's capability is toggled off in Settings. */ + /** Replaces the connected status when the capability is off in Settings. */ disabledReason?: string; } const CoreAgentCard: React.FC = ({ - agent, - index = 0, - meta, - toolCount, - skillCount = 0, - subagentCount = 0, - onOpenDetails, - disabledReason, + agent, meta, disabledReason, onOpenDetails, ...metrics }) => { const { t } = useTranslation('scenes/agents'); - const agentIcon = AGENT_ICON_MAP[(agent.iconKey ?? 'bot') as keyof typeof AGENT_ICON_MAP] ?? { glyph: Bot }; - const totalTools = toolCount ?? agent.toolCount ?? agent.defaultTools?.length ?? 0; - const openDetails = () => onOpenDetails(agent); const statusLabel = disabledReason ?? t('agentCard.status.connected'); + const agentIcon = AGENT_ICON_MAP[(agent.iconKey ?? 'bot') as keyof typeof AGENT_ICON_MAP] ?? { glyph: Bot }; return ( -
    { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - openDetails(); - } - }} - aria-label={agent.name} - data-testid="agent-list-item" - data-agent-id={agent.id} - data-agent-name={agent.name} - data-agent-kind={agent.agentKind} + -
    -
    - -
    -
    - -
    -
    -
    - {agent.name} - - - {meta.role} - - -
    - - - {statusLabel} + -
    - -
    - - {getAgentDescription(t, agent)} - -
    - -
    -
    - - - {t('agentCard.metrics.tools')} - {totalTools} - - {agent.agentKind === 'mode' ? ( - <> - - - {t('agentCard.metrics.skills')} - {skillCount} - - - - {t('agentCard.metrics.collaboration')} - {subagentCount} - - - ) : null} - {agent.agentKind === 'subagent' && agent.subagentModelDisplayName ? ( - - - {t('agentCard.metrics.model')} - - {agent.subagentModelDisplayName} - - - ) : null} + )} + title={( +
    + + {agent.name} +
    -
    -
    -
    + )} + description={( + + {meta.role} + + )} + /> + + + {getAgentDescription(t, agent)} + + + + + + + {statusLabel} + + +
    ); }; diff --git a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx index 82d086c2d9..71cb6ca863 100644 --- a/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx +++ b/src/web-ui/src/app/scenes/agents/components/CreateAgentPage.tsx @@ -615,7 +615,7 @@ const CreateAgentPage: React.FC = () => { onClick={() => setKind(candidateKind)} > {candidateKind === 'mode' - ? t('filters.mode') + ? t('filters.agent') : t('filters.subagent')} diff --git a/src/web-ui/src/app/scenes/agents/components/HarnessProfileStep.appearance.ts b/src/web-ui/src/app/scenes/agents/components/HarnessProfileStep.appearance.ts index a3a9c386aa..a9b2c7efc4 100644 --- a/src/web-ui/src/app/scenes/agents/components/HarnessProfileStep.appearance.ts +++ b/src/web-ui/src/app/scenes/agents/components/HarnessProfileStep.appearance.ts @@ -7,7 +7,7 @@ export const harnessProfileStepAppearanceDescriptor: AppearanceSurfaceDescriptor { id: 'profile', attribute: 'data-openbitfun-profile', - values: ['minimal', 'balanced', 'ultimate', 'creative'], + values: ['Minimal', 'Standard', 'Ultimate', 'Creative'], }, ], }; diff --git a/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.appearance.ts b/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.appearance.ts index 0dd41d8381..c13798f9d3 100644 --- a/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.appearance.ts +++ b/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.appearance.ts @@ -2,11 +2,11 @@ import type { AppearanceSurfaceDescriptor } from '@/infrastructure/appearance'; export const skillGroupPickerAppearanceDescriptor: AppearanceSurfaceDescriptor = { id: 'skill-group-picker', + componentAttribute: 'data-openbitfun-product-component', parts: [ { id: 'root' }, { id: 'head' }, { id: 'sections' }, { id: 'section' }, { id: 'group' }, { id: 'groupHeader' }, { id: 'groupActions' }, - { id: 'tokenGrid' }, { id: 'token' }, { id: 'manager' }, - { id: 'managerEditor' }, { id: 'managerList' }, { id: 'managerGroup' }, + { id: 'tokenGrid' }, { id: 'token' }, { id: 'summary' }, { id: 'summaryGroup' }, { id: 'empty' }, ], states: [ diff --git a/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.scss b/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.scss index 6da3ddc12a..9490209413 100644 --- a/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.scss +++ b/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.scss @@ -1,294 +1,8 @@ -.skill-group-picker { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-3); +@use './AgentCapabilityGroups' as capability; - &__head, - &__group-head, - &__section-label, - &__group-actions, - &__footer { - display: flex; - align-items: center; - } +@include capability.capability-group-layout('skill'); - &__head { - justify-content: space-between; - gap: var(--openbitfun-space-2); - } - - &__selected-count { - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); - font-variant-numeric: tabular-nums; - } - - &__sections { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-3); - } - - &__section { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-1); - } - - &__section-label { - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-meta-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - text-transform: uppercase; - letter-spacing: var(--openbitfun-type-modifier-tracking-wider-letter-spacing); - } - - &__group { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-1); - padding: var(--openbitfun-space-2); - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-base); - background: var(--openbitfun-color-surface-subtle); - } - - &__group-head { - justify-content: space-between; - gap: var(--openbitfun-space-2); - } - - &__group-title-wrap { - display: inline-flex; - align-items: center; - min-width: 0; - gap: var(--openbitfun-space-2); - } - - &__group-name { - min-width: 0; - overflow: hidden; - white-space: nowrap; - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-meta-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - text-transform: uppercase; - letter-spacing: var(--openbitfun-type-modifier-tracking-wide-letter-spacing); - } - - &__group-count { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 22px; - padding: 1px 6px; - border-radius: var(--openbitfun-radius-pill); - background: var(--openbitfun-color-action-neutral-surface-hover); - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-micro-font-size); - font-variant-numeric: tabular-nums; - white-space: nowrap; - } - - &__group-actions { - flex-shrink: 0; - gap: var(--openbitfun-space-1); - flex-wrap: wrap; - justify-content: flex-end; - } - - &__token-grid, - &__skills { - display: flex; - flex-wrap: wrap; - gap: var(--openbitfun-space-1); - } - - &__token { - display: inline-flex; - align-items: center; - max-width: 100%; - min-height: 26px; - padding: 3px 8px; - border: 1px solid transparent; - border-radius: var(--openbitfun-radius-pill); - background: transparent; - color: var(--openbitfun-color-content-muted); - font-family: var(--openbitfun-type-code-md-font-family); - font-size: var(--openbitfun-type-meta-font-size); - overflow: hidden; - white-space: nowrap; - cursor: pointer; - transition: - border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - &:hover:not(.is-on):not(:disabled) { - border-color: var(--openbitfun-color-field-border-hover); - color: var(--openbitfun-color-content-secondary); - } - - &.is-on { - border-color: color-mix(in srgb, var(--openbitfun-color-accent-default) 40%, transparent); - background: var(--openbitfun-color-action-secondary-background); - color: var(--openbitfun-color-accent-default); - } - - &:disabled { - cursor: not-allowed; - opacity: 0.6; - } - } -} - -.skill-group-summary { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-2); - - &__group { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-1); - } - - &__label { - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - } - - &__skills { - display: flex; - flex-wrap: wrap; - gap: var(--openbitfun-space-1); - } -} - -.skill-group-manager { - display: flex; - box-sizing: border-box; - max-height: min(620px, 70vh); - padding-block: var(--openbitfun-space-3); - flex-direction: column; - gap: var(--openbitfun-space-3); - - &__head, - &__group-row, - &__group-actions, - &__footer { - display: flex; - align-items: center; - } - - &__head { - justify-content: space-between; - gap: var(--openbitfun-space-3); - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-md-font-size); - } - - &__list { - border: 1px solid var(--openbitfun-color-border-subtle); - } - - &__group-row { - justify-content: space-between; - gap: var(--openbitfun-space-3); - padding: var(--openbitfun-space-2) var(--openbitfun-space-3); - } - - &__group-row + &__group-row { - border-top: 1px solid var(--openbitfun-color-border-subtle); - } - - &__group-copy { - display: flex; - min-width: 0; - flex: 1; - flex-direction: column; - gap: 2px; - } - - &__group-name { - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-label-md-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); - } - - &__group-meta, - &__empty { - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-label-sm-font-size); - } - - &__group-actions { - flex-shrink: 0; - gap: 2px; - } - - &__editor { - display: flex; - min-height: 0; - flex: 1; - flex-direction: column; - gap: var(--openbitfun-space-3); - } - - &__field { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-1); - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); - } - - &__token-grid { - min-height: 0; - max-height: 360px; - display: flex; - flex-wrap: wrap; - gap: var(--openbitfun-space-1); - border: 1px solid var(--openbitfun-color-border-subtle); - padding: var(--openbitfun-space-2); - } - - &__token { - display: inline-flex; - align-items: center; - max-width: 100%; - min-height: 26px; - padding: 3px 8px; - border: 1px solid transparent; - border-radius: var(--openbitfun-radius-pill); - background: transparent; - color: var(--openbitfun-color-content-muted); - font-family: var(--openbitfun-type-code-md-font-family); - font-size: var(--openbitfun-type-meta-font-size); - overflow: hidden; - white-space: nowrap; - cursor: pointer; - transition: - border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - &:hover:not(.is-on):not(:disabled) { - border-color: var(--openbitfun-color-field-border-hover); - color: var(--openbitfun-color-content-secondary); - } - - &.is-on { - border-color: color-mix(in srgb, var(--openbitfun-color-accent-default) 40%, transparent); - background: var(--openbitfun-color-action-secondary-background); - color: var(--openbitfun-color-accent-default); - } - - &:disabled { - cursor: not-allowed; - opacity: 0.6; - } - } +.skill-group-picker__unavailable { + color: var(--openbitfun-color-content-muted); + font-size: var(--openbitfun-type-meta-font-size); } diff --git a/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.tsx b/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.tsx index e77921af3f..ffc9b0bb5d 100644 --- a/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.tsx +++ b/src/web-ui/src/app/scenes/agents/components/SkillGroupPicker.tsx @@ -1,51 +1,21 @@ -import { OverflowText, - Button, - Icon, - IconButton, - Input, - ScrollArea, - Switch, - Tooltip, - Dialog, - DialogBody, - DialogClose, - DialogHeader, - DialogHeading, - DialogTitle, -} from '@openbitfun/ui'; -import React, { useMemo, useState } from 'react'; -import type { TFunction } from 'i18next'; - -import { useTranslation } from 'react-i18next'; - -import { confirmDanger } from '@/infrastructure/confirm-dialog'; +import { Button, Checkbox, FieldGroup, FieldRow, FormSection, OverflowText, StatusPill, Toolbar } from '@openbitfun/ui'; +import React, { useMemo } from 'react'; +import { useI18n, type UseI18nReturn } from '@/infrastructure/i18n/hooks/useI18n'; import type { UserSkillGroup } from '@/infrastructure/config/types'; -import { useNotification } from '@/shared/notification-system'; import { - type GroupableSkill, - type ResolvedSkillGroup, - builtinSkillGroupLabelKey, - resolveSkillGroupSummary, - resolveSkillGroups, - setSkillGroupSelection, - skillGroupKeys, - toggleSkillSelection, - unavailableUserSkillKeys, -} from './skillGroups'; -import { - AgentCapabilityTooltip, - type AgentCapabilityTooltipField, -} from './AgentCapabilityTooltip'; + type GroupableSkill, type ResolvedSkillGroup, builtinSkillGroupLabelKey, + resolveSkillGroupSummary, resolveSkillSelectionGroups, setSkillGroupSelection, + skillGroupKeys, toggleSkillSelection, +} from '@/features/skill-groups/skillGroups'; +import { AgentCapabilityTooltip, type AgentCapabilityTooltipField } from './AgentCapabilityTooltip'; import { capabilityTooltipAriaLabel } from './agentCapabilityTooltipUtils'; import './SkillGroupPicker.scss'; interface SkillGroupPickerProps { skills: GroupableSkill[]; - managementSkills?: GroupableSkill[]; selectedSkillKeys: readonly string[]; userGroups: UserSkillGroup[]; onSelectionChange: (skillKeys: string[]) => void; - onSaveUserGroups: (groups: UserSkillGroup[]) => Promise; disabled?: boolean; testId?: string; } @@ -56,28 +26,6 @@ interface SkillGroupSummaryProps { userGroups: UserSkillGroup[]; } -export interface SkillGroupManagerModalProps { - isOpen: boolean; - onClose: () => void; - skills: GroupableSkill[]; - groups: UserSkillGroup[]; - onSaveGroups: (groups: UserSkillGroup[]) => Promise; -} - -function createGroupId(): string { - if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { - return crypto.randomUUID(); - } - return `skill_group_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; -} - -function hasDuplicateName(groups: UserSkillGroup[], name: string, exceptId?: string): boolean { - const normalized = name.trim().toLocaleLowerCase(); - return groups.some((group) => ( - group.id !== exceptId && group.name.trim().toLocaleLowerCase() === normalized - )); -} - function isGroupEnabled(group: ResolvedSkillGroup, selectedSkillKeys: readonly string[]): boolean { const selected = new Set(selectedSkillKeys); return group.skills.length > 0 && group.skills.every((skill) => selected.has(skill.key)); @@ -88,12 +36,12 @@ function selectedGroupSkillCount(group: ResolvedSkillGroup, selectedSkillKeys: r return group.skills.filter((skill) => selected.has(skill.key)).length; } -function builtinGroupLabel(groupKey: string, t: TFunction<'scenes/agents'>): string { +function builtinGroupLabel(groupKey: string, t: UseI18nReturn['t']): string { const labelKey = builtinSkillGroupLabelKey(groupKey); return labelKey ? t(`agentsOverview.skillGroups.${labelKey}`) : groupKey; } -function groupSectionLabel(group: ResolvedSkillGroup, t: TFunction<'scenes/agents'>): string { +function groupSectionLabel(group: ResolvedSkillGroup, t: UseI18nReturn['t']): string { switch (group.kind) { case 'user': return t('agentsOverview.skillGroupPicker.myGroups'); @@ -122,7 +70,7 @@ function skillDisplayName(skill: GroupableSkill, duplicateNames: Set): s function skillTooltipFields( skill: GroupableSkill, - t: TFunction<'scenes/agents'>, + t: UseI18nReturn['t'], ): AgentCapabilityTooltipField[] { const source = [skill.sourceLabel ?? skill.sourceSlot, skill.level].filter(Boolean).join('/'); return [ @@ -146,318 +94,17 @@ function skillTooltipFields( ]; } -export const SkillGroupManagerModal: React.FC = ({ - isOpen, - onClose, - skills, - groups, - onSaveGroups, -}) => { - const { t } = useTranslation('scenes/agents'); - const notification = useNotification(); - const [editingId, setEditingId] = useState(null); - const [name, setName] = useState(''); - const [nameError, setNameError] = useState(false); - const [skillKeys, setSkillKeys] = useState>(new Set()); - const [saving, setSaving] = useState(false); - const duplicateNames = useMemo(() => duplicateSkillNames(skills), [skills]); - const selectableSkills = useMemo( - () => [...skills].sort((left, right) => left.name.localeCompare(right.name) || left.key.localeCompare(right.key)), - [skills], - ); - const editingGroup = groups.find((group) => group.id === editingId) ?? null; - const isEditing = editingId !== null; - - const closeEditor = () => { - setEditingId(null); - setName(''); - setNameError(false); - setSkillKeys(new Set()); - }; - - const startCreate = () => { - setEditingId('__new__'); - setName(''); - setNameError(false); - setSkillKeys(new Set()); - }; - - const startEdit = (group: UserSkillGroup) => { - setEditingId(group.id); - setName(group.name); - setNameError(false); - setSkillKeys(new Set(group.skillKeys)); - }; - - const toggleSkill = (skillKey: string) => { - setSkillKeys((current) => { - const next = new Set(current); - if (next.has(skillKey)) { - next.delete(skillKey); - } else { - next.add(skillKey); - } - return next; - }); - }; - - const saveEditor = async () => { - const trimmedName = name.trim(); - const selectedKeys = Array.from(skillKeys); - const existingId = editingGroup?.id; - if (!trimmedName) { - setNameError(true); - return; - } - if (hasDuplicateName(groups, trimmedName, existingId)) { - notification.error(t('agentsOverview.skillGroupPicker.validation.nameDuplicate')); - return; - } - if (selectedKeys.length === 0) { - notification.error(t('agentsOverview.skillGroupPicker.validation.skillsRequired')); - return; - } - - const nextGroup: UserSkillGroup = { - id: existingId ?? createGroupId(), - name: trimmedName, - skillKeys: selectedKeys, - }; - const nextGroups = existingId - ? groups.map((group) => group.id === existingId ? nextGroup : group) - : [...groups, nextGroup]; - - setSaving(true); - try { - await onSaveGroups(nextGroups); - closeEditor(); - } catch { - notification.error(t('agentsOverview.skillGroupPicker.saveFailed')); - } finally { - setSaving(false); - } - }; - - const deleteGroup = async (group: UserSkillGroup) => { - const confirmed = await confirmDanger( - t('agentsOverview.skillGroupPicker.deleteTitle'), - t('agentsOverview.skillGroupPicker.deleteMessage', { name: group.name }), - { confirmText: t('agentsOverview.skillGroupPicker.deleteConfirm') }, - ); - if (!confirmed) { - return; - } - setSaving(true); - try { - await onSaveGroups(groups.filter((candidate) => candidate.id !== group.id)); - if (editingId === group.id) { - closeEditor(); - } - } catch { - notification.error(t('agentsOverview.skillGroupPicker.saveFailed')); - } finally { - setSaving(false); - } - }; - - const moveGroup = async (index: number, direction: -1 | 1) => { - const nextIndex = index + direction; - if (nextIndex < 0 || nextIndex >= groups.length) { - return; - } - const nextGroups = [...groups]; - [nextGroups[index], nextGroups[nextIndex]] = [nextGroups[nextIndex], nextGroups[index]]; - setSaving(true); - try { - await onSaveGroups(nextGroups); - } catch { - notification.error(t('agentsOverview.skillGroupPicker.saveFailed')); - } finally { - setSaving(false); - } - }; - - return ( - { - if (!nextOpen) { - closeEditor(); - onClose(); - } - }} - size="lg" - data-testid="skill-group-manager" - > - - - {t('agentsOverview.skillGroupPicker.manageTitle')} - - - - -
    - {isEditing ? ( -
    -
    - - { - setName(event.target.value); - if (nameError) { - setNameError(false); - } - }} - placeholder={t('agentsOverview.skillGroupPicker.groupNamePlaceholder')} - invalid={nameError} - disabled={saving} - size="sm" - /> -
    -
    - {t('agentsOverview.skillGroupPicker.groupSkills')} - - {selectableSkills.map((skill) => { - const selected = skillKeys.has(skill.key); - const tooltipFields = skillTooltipFields(skill, t); - return ( - - - - ); - })} - -
    -
    - - -
    -
    - ) : ( - <> -
    - {t('agentsOverview.skillGroupPicker.manageSubtitle')} - -
    - {groups.length === 0 ? ( -

    {t('agentsOverview.skillGroupPicker.noUserGroups')}

    - ) : ( - - {groups.map((group, index) => { - const unavailable = unavailableUserSkillKeys(group, skills); - return ( -
    -
    - {group.name} - - {t('agentsOverview.skillGroupPicker.groupCount', { count: group.skillKeys.length })} - {unavailable.length > 0 - ? ` · ${t('agentsOverview.skillGroupPicker.unavailableCount', { count: unavailable.length })}` - : ''} - -
    -
    - - void moveGroup(index, -1)} - disabled={saving || index === 0} - icon={} - /> - - - void moveGroup(index, 1)} - disabled={saving || index === groups.length - 1} - icon={} - /> - - - startEdit(group)} - disabled={saving} - icon={} - /> - - - void deleteGroup(group)} - disabled={saving} - icon={} - /> - -
    -
    - ); - })} -
    - )} - - )} -
    -
    -
    - ); -}; - export const SkillGroupPicker: React.FC = ({ skills, - managementSkills, selectedSkillKeys, userGroups, onSelectionChange, - onSaveUserGroups, disabled = false, testId, }) => { - const { t } = useTranslation('scenes/agents'); - const [isManagerOpen, setIsManagerOpen] = useState(false); + const { t, formatNumber } = useI18n('scenes/agents'); const duplicateNames = useMemo(() => duplicateSkillNames(skills), [skills]); - const groups = useMemo(() => resolveSkillGroups(skills, userGroups, { + const groups = useMemo(() => resolveSkillSelectionGroups(skills, userGroups, { builtin: (groupKey) => builtinGroupLabel(groupKey, t), other: t('agentsOverview.skillGroupPicker.otherSkills'), }), [skills, t, userGroups]); @@ -474,115 +121,114 @@ export const SkillGroupPicker: React.FC = ({ }, [groups, t]); return ( -
    -
    - +
    + {t('agentsOverview.skillGroupPicker.selectedCount', { count: selectedCount })} - - -
    -
    + } + /> +
    {sections.map(([sectionLabel, sectionGroups]) => ( -
    - {sectionLabel} + {sectionGroups.map((group) => { const selectedInGroup = selectedGroupSkillCount(group, selectedSkillKeys); const allSelected = isGroupEnabled(group, selectedSkillKeys); return ( -
    -
    -
    - {group.label} - - {selectedInGroup}/{group.skills.length} - -
    -
    - {selectedInGroup > 0 && !allSelected ? ( - - ) : null} - onSelectionChange( - setSkillGroupSelection( - selectedSkillKeys, - skillGroupKeys(group), - event.target.checked, - ), + + +
    +
    + {group.label} + + {formatNumber(selectedInGroup)}/{formatNumber(group.skills.length)} + + {group.unavailableSkillKeys.length > 0 && ( + + {t('agentsOverview.skillGroupPicker.unavailableCount', { count: group.unavailableSkillKeys.length })} + )} - disabled={disabled} - aria-label={allSelected - ? t('agentsOverview.skillGroupPicker.clearGroupSkills', { name: group.label }) - : t('agentsOverview.skillGroupPicker.enableGroupSkills', { name: group.label })} - /> -
    -
    -
    - {group.skills.map((skill) => { - const selected = selectedSkillKeys.includes(skill.key); - const tooltipFields = skillTooltipFields(skill, t); - return ( - - - - ); - })} -
    -
    + > + {t('agentsOverview.clearGroup')} + + ) : null} + 0 && !allSelected} + checked={allSelected} + onCheckedChange={(checked) => onSelectionChange( + setSkillGroupSelection( + selectedSkillKeys, + skillGroupKeys(group), + checked, + ), + )} + disabled={disabled || group.skills.length === 0} + aria-label={allSelected + ? t('agentsOverview.skillGroupPicker.clearGroupSkills', { name: group.label }) + : t('agentsOverview.skillGroupPicker.enableGroupSkills', { name: group.label })} + /> +
    +
    + + +
    + {group.skills.map((skill) => { + const selected = selectedSkillKeys.includes(skill.key); + const tooltipFields = skillTooltipFields(skill, t); + return ( + + + + ); + })} +
    +
    + ); })} -
    + ))}
    - setIsManagerOpen(false)} - skills={managementSkills ?? skills} - groups={userGroups} - onSaveGroups={onSaveUserGroups} - />
    ); }; @@ -592,7 +238,7 @@ export const SkillGroupSummary: React.FC = ({ selectedSkillKeys, userGroups, }) => { - const { t } = useTranslation('scenes/agents'); + const { t } = useI18n('scenes/agents'); const duplicateNames = useMemo(() => duplicateSkillNames(skills), [skills]); const groups = useMemo(() => resolveSkillGroupSummary(skills, userGroups, selectedSkillKeys, { builtin: (groupKey) => builtinGroupLabel(groupKey, t), @@ -600,14 +246,14 @@ export const SkillGroupSummary: React.FC = ({ }), [selectedSkillKeys, skills, t, userGroups]); if (groups.length === 0) { - return {t('agentsOverview.noSkills')}; + return {t('agentsOverview.noSkills')}; } return ( -
    +
    {groups.map((group) => ( -
    - {group.label} +
    {group.skills.map((skill) => { const tooltipFields = skillTooltipFields(skill, t); @@ -618,14 +264,14 @@ export const SkillGroupSummary: React.FC = ({ description={skill.description} fields={tooltipFields} > - + {skillDisplayName(skill, duplicateNames)} - + ); })}
    -
    + ))}
    ); diff --git a/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.appearance.ts b/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.appearance.ts index a92bac9714..59d3ce6204 100644 --- a/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.appearance.ts +++ b/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.appearance.ts @@ -2,6 +2,7 @@ import type { AppearanceSurfaceDescriptor } from '@/infrastructure/appearance'; export const toolGroupPickerAppearanceDescriptor: AppearanceSurfaceDescriptor = { id: 'tool-group-picker', + componentAttribute: 'data-openbitfun-product-component', parts: [ { id: 'root' }, { id: 'head' }, { id: 'sections' }, { id: 'section' }, { id: 'group' }, { id: 'groupHeader' }, { id: 'groupActions' }, diff --git a/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.scss b/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.scss index 92265960e4..810c74b889 100644 --- a/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.scss +++ b/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.scss @@ -1,306 +1,65 @@ -.tool-group-picker { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-3); - - &__head, - &__group-head, - &__section-label, - &__group-actions, - &__footer { - display: flex; - align-items: center; - } - - &__head { - justify-content: space-between; - gap: var(--openbitfun-space-2); - } - - &__selected-count { - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); - font-variant-numeric: tabular-nums; - } - - &__sections { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-3); - } +@use './AgentCapabilityGroups' as capability; - &__section { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-1); - } - - &__section-label { - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-meta-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - text-transform: uppercase; - letter-spacing: var(--openbitfun-type-modifier-tracking-wider-letter-spacing); - } +@include capability.capability-group-layout('tool'); - &__group { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-1); - padding: var(--openbitfun-space-2); - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-base); - background: var(--openbitfun-color-surface-subtle); - } - - &__group-head { - justify-content: space-between; - gap: var(--openbitfun-space-2); - } - - &__group-title-wrap { - display: inline-flex; - align-items: center; - min-width: 0; - gap: var(--openbitfun-space-2); - } - - &__group-name { - min-width: 0; - overflow: hidden; - white-space: nowrap; - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-meta-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - text-transform: uppercase; - letter-spacing: var(--openbitfun-type-modifier-tracking-wide-letter-spacing); - } - - &__group-count { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 22px; - padding: 1px 6px; - border-radius: var(--openbitfun-radius-pill); - background: var(--openbitfun-color-action-neutral-surface-hover); - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-micro-font-size); - font-variant-numeric: tabular-nums; - white-space: nowrap; - } - - &__group-actions { - flex-shrink: 0; - gap: var(--openbitfun-space-1); - flex-wrap: wrap; - justify-content: flex-end; - } - - &__token-grid { - display: flex; - flex-wrap: wrap; - gap: var(--openbitfun-space-1); - } - - &__token { - display: inline-flex; - align-items: center; - max-width: 100%; - min-height: 26px; - padding: 3px 8px; - border: 1px solid transparent; - border-radius: var(--openbitfun-radius-pill); - background: transparent; - color: var(--openbitfun-color-content-muted); - font-family: var(--openbitfun-type-code-md-font-family); - font-size: var(--openbitfun-type-meta-font-size); - overflow: hidden; - white-space: nowrap; - cursor: pointer; - transition: - border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - &:hover:not(.is-on):not(:disabled) { - border-color: var(--openbitfun-color-field-border-hover); - color: var(--openbitfun-color-content-secondary); - } - - &.is-on { - border-color: color-mix(in srgb, var(--openbitfun-color-accent-default) 40%, transparent); - background: var(--openbitfun-color-action-secondary-background); - color: var(--openbitfun-color-accent-default); - } - - &:disabled { - cursor: not-allowed; - opacity: 0.6; - } - } -} - -.tool-group-summary { +.tool-group-manager { display: flex; + min-width: 0; flex-direction: column; - gap: var(--openbitfun-space-2); + gap: var(--openbitfun-space-4); - &__group { + &__editor { display: flex; + min-height: 0; flex-direction: column; - gap: var(--openbitfun-space-1); - } - - &__label { - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); + gap: var(--openbitfun-space-4); } - &__tools { - display: flex; - flex-wrap: wrap; - gap: var(--openbitfun-space-1); - } -} - -.tool-group-manager { - display: flex; - box-sizing: border-box; - max-height: min(620px, 70vh); - padding-block: var(--openbitfun-space-3); - flex-direction: column; - gap: var(--openbitfun-space-3); - - &__head, &__group-row, - &__group-actions, - &__footer { + &__group-actions { display: flex; align-items: center; - } - - &__head { - justify-content: space-between; - gap: var(--openbitfun-space-3); - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-md-font-size); - } - - &__list { - border: 1px solid var(--openbitfun-color-border-subtle); + gap: var(--openbitfun-space-2); } &__group-row { + flex-wrap: wrap; justify-content: space-between; - gap: var(--openbitfun-space-3); - padding: var(--openbitfun-space-2) var(--openbitfun-space-3); - } - - &__group-row + &__group-row { - border-top: 1px solid var(--openbitfun-color-border-subtle); } &__group-copy { display: flex; min-width: 0; - flex: 1; + flex: 1 1 auto; flex-direction: column; - gap: 2px; + gap: var(--openbitfun-space-1); } &__group-name { color: var(--openbitfun-color-content-primary); font-size: var(--openbitfun-type-label-md-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); + line-height: var(--openbitfun-type-label-md-line-height); } &__group-meta, &__empty { + margin: 0; color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-label-sm-font-size); + font-size: var(--openbitfun-type-meta-font-size); + line-height: var(--openbitfun-type-meta-line-height); } &__group-actions { - flex-shrink: 0; - gap: 2px; - } - - &__editor { - display: flex; - min-height: 0; - flex: 1; - flex-direction: column; - gap: var(--openbitfun-space-3); - } - - &__field { - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-1); - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); + flex: 0 0 auto; } &__token-grid { - min-height: 0; - max-height: 360px; display: flex; flex-wrap: wrap; - gap: var(--openbitfun-space-1); - border: 1px solid var(--openbitfun-color-border-subtle); - padding: var(--openbitfun-space-2); + gap: var(--openbitfun-space-2); } &__token { - display: inline-flex; - align-items: center; max-width: 100%; - min-height: 26px; - padding: 3px 8px; - border: 1px solid transparent; - border-radius: var(--openbitfun-radius-pill); - background: transparent; - color: var(--openbitfun-color-content-muted); - font-family: var(--openbitfun-type-code-md-font-family); - font-size: var(--openbitfun-type-meta-font-size); - overflow: hidden; - white-space: nowrap; - cursor: pointer; - transition: - border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - &:hover:not(.is-on):not(:disabled) { - border-color: var(--openbitfun-color-field-border-hover); - color: var(--openbitfun-color-content-secondary); - } - - &.is-on { - border-color: color-mix(in srgb, var(--openbitfun-color-accent-default) 40%, transparent); - background: var(--openbitfun-color-action-secondary-background); - color: var(--openbitfun-color-accent-default); - } - - &:disabled { - cursor: not-allowed; - opacity: 0.6; - } - } - - &__footer { - justify-content: flex-end; - gap: var(--openbitfun-space-2); - padding-top: var(--openbitfun-space-2); - border-top: 1px solid var(--openbitfun-color-border-subtle); - } -} - -@container (max-width: 520px) { - .tool-group-picker__group-head { - gap: var(--openbitfun-space-1); } } diff --git a/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.tsx b/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.tsx index 15ee01e112..90d63f6e63 100644 --- a/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.tsx +++ b/src/web-ui/src/app/scenes/agents/components/ToolGroupPicker.tsx @@ -1,10 +1,17 @@ -import { OverflowText, +import { + OverflowText, + Checkbox, + DialogFooter, + Field, + FieldGroup, + FieldRow, + FormSection, + StatusPill, + Toolbar, Button, Icon, IconButton, Input, - ScrollArea, - Switch, Tooltip, Dialog, DialogBody, @@ -14,9 +21,8 @@ import { OverflowText, DialogTitle, } from '@openbitfun/ui'; import React, { useMemo, useState } from 'react'; -import type { TFunction } from 'i18next'; -import { useTranslation } from 'react-i18next'; +import { useI18n, type UseI18nReturn } from '@/infrastructure/i18n/hooks/useI18n'; import { confirmDanger } from '@/infrastructure/confirm-dialog'; import type { UserToolGroup } from '@/infrastructure/config/types'; @@ -79,7 +85,7 @@ function selectedGroupToolCount(group: ResolvedToolGroup, selectedToolNames: rea return group.tools.filter((tool) => selected.has(tool.name)).length; } -function groupSectionLabel(group: ResolvedToolGroup, t: TFunction<'scenes/agents'>): string { +function groupSectionLabel(group: ResolvedToolGroup, t: UseI18nReturn['t']): string { switch (group.kind) { case 'user': return t('agentsOverview.toolGroups.myGroups'); @@ -94,7 +100,7 @@ function groupSectionLabel(group: ResolvedToolGroup, t: TFunction<'scenes/agents function toolTooltipFields( tool: GroupableTool, - t: TFunction<'scenes/agents'>, + t: UseI18nReturn['t'], ): AgentCapabilityTooltipField[] { const mcpServerName = tool.dynamic_info?.mcp?.serverName?.trim(); const providerId = tool.dynamic_info?.providerId?.trim(); @@ -136,7 +142,7 @@ const GroupManagerModal: React.FC = ({ groups, onSaveGroups, }) => { - const { t } = useTranslation('scenes/agents'); + const { t } = useI18n('scenes/agents'); const notification = useNotification(); const [editingId, setEditingId] = useState(null); const [name, setName] = useState(''); @@ -279,147 +285,150 @@ const GroupManagerModal: React.FC = ({ -
    - {isEditing ? ( -
    -
    - - { - setName(event.target.value); - if (nameError) { - setNameError(false); - } - }} - placeholder={t('agentsOverview.toolGroups.groupNamePlaceholder')} - invalid={nameError} - disabled={saving} - size="sm" - /> -
    -
    - {t('agentsOverview.toolGroups.groupTools')} - - {selectableTools.map((tool) => { - const selected = toolNames.has(tool.name); - const tooltipFields = toolTooltipFields(tool, t); - return ( - - - - ); - })} - -
    -
    - - -
    -
    - ) : ( - <> -
    - {t('agentsOverview.toolGroups.manageSubtitle')} - +
    + {isEditing ? ( +
    + + { + setName(event.target.value); + if (nameError) setNameError(false); + }} + placeholder={t('agentsOverview.toolGroups.groupNamePlaceholder')} + invalid={nameError} + disabled={saving} + size="sm" + /> + + +
    + {selectableTools.map((tool) => { + const selected = toolNames.has(tool.name); + const tooltipFields = toolTooltipFields(tool, t); + return ( + + + + ); + })} +
    +
    - {groups.length === 0 ? ( -

    {t('agentsOverview.toolGroups.noUserGroups')}

    - ) : ( - - {groups.map((group, index) => { - const unavailable = unavailableUserToolNames(group, tools); - return ( -
    -
    - {group.name} - - {t('agentsOverview.toolGroups.groupCount', { count: group.toolNames.length })} - {unavailable.length > 0 - ? ` · ${t('agentsOverview.toolGroups.unavailableCount', { count: unavailable.length })}` - : ''} - -
    -
    - - void moveGroup(index, -1)} - disabled={saving || index === 0} - icon={} - /> - - - void moveGroup(index, 1)} - disabled={saving || index === groups.length - 1} - icon={} - /> - - - startEdit(group)} - disabled={saving} - icon={} - /> - - - void deleteGroup(group)} - disabled={saving} - icon={} - /> - -
    -
    - ); - })} -
    - )} - - )} -
    - + ) : ( + } + > + {t('agentsOverview.toolGroups.createGroup')} + + )} + > + {groups.length === 0 ? ( +

    {t('agentsOverview.toolGroups.noUserGroups')}

    + ) : ( + + {groups.map((group, index) => { + const unavailable = unavailableUserToolNames(group, tools); + return ( + +
    +
    + {group.name} + + {t('agentsOverview.toolGroups.groupCount', { count: group.toolNames.length })} + {unavailable.length > 0 + ? ` · ${t('agentsOverview.toolGroups.unavailableCount', { count: unavailable.length })}` + : ''} + +
    +
    + + void moveGroup(index, -1)} + disabled={saving || index === 0} + icon={} + /> + + + void moveGroup(index, 1)} + disabled={saving || index === groups.length - 1} + icon={} + /> + + + startEdit(group)} + disabled={saving} + icon={} + /> + + + void deleteGroup(group)} + disabled={saving} + icon={} + /> + +
    +
    +
    + ); + })} +
    + )} +
    + )} +
    + + {isEditing ? ( + + + + + ) : null} ); }; @@ -434,7 +443,7 @@ export const ToolGroupPicker: React.FC = ({ disabled = false, testId, }) => { - const { t } = useTranslation('scenes/agents'); + const { t, formatNumber } = useI18n('scenes/agents'); const [isManagerOpen, setIsManagerOpen] = useState(false); const groups = useMemo(() => resolveToolGroups(tools, userGroups, t), [t, tools, userGroups]); const selectedCount = new Set(selectedToolNames).size; @@ -450,102 +459,112 @@ export const ToolGroupPicker: React.FC = ({ }, [groups, t]); return ( -
    -
    - - {t('agentsOverview.toolGroups.selectedCount', { count: selectedCount })} - - -
    -
    +
    + + {t('agentsOverview.toolGroups.selectedCount', { count: selectedCount })} + + )} + trailing={( + + )} + /> +
    {sections.map(([sectionLabel, sectionGroups]) => ( -
    - {sectionLabel} + {sectionGroups.map((group) => { const selectedInGroup = selectedGroupToolCount(group, selectedToolNames); const allSelected = isGroupEnabled(group, selectedToolNames); return ( -
    -
    -
    - {group.label} - - {selectedInGroup}/{group.tools.length} - -
    -
    - {selectedInGroup > 0 && !allSelected ? ( - - ) : null} - onSelectionChange( - setToolGroupSelection( - selectedToolNames, - groupToolNames(group), - event.target.checked, - ), - )} - disabled={disabled} - aria-label={allSelected - ? t('agentsOverview.toolGroups.clearGroupTools', { name: group.label }) - : t('agentsOverview.toolGroups.enableGroupTools', { name: group.label })} - /> -
    -
    -
    - {group.tools.map((tool) => { - const selected = selectedToolNames.includes(tool.name); - const tooltipFields = toolTooltipFields(tool, t); - return ( - - - - ); - })} -
    -
    + > + {t('agentsOverview.clearGroup')} + + ) : null} + 0 && !allSelected} + onCheckedChange={(checked) => onSelectionChange( + setToolGroupSelection(selectedToolNames, groupToolNames(group), checked), + )} + disabled={disabled || group.tools.length === 0} + aria-label={allSelected + ? t('agentsOverview.toolGroups.clearGroupTools', { name: group.label }) + : t('agentsOverview.toolGroups.enableGroupTools', { name: group.label })} + /> +
    +
    + + +
    + {group.tools.map((tool) => { + const selected = selectedToolNames.includes(tool.name); + const tooltipFields = toolTooltipFields(tool, t); + return ( + + + + ); + })} +
    +
    + ); })} - + ))}
    = ({ selectedToolNames, userGroups, }) => { - const { t } = useTranslation('scenes/agents'); + const { t } = useI18n('scenes/agents'); const groups = useMemo( () => resolveToolGroupSummary(tools, userGroups, selectedToolNames, t), [selectedToolNames, t, tools, userGroups], ); if (groups.length === 0) { - return {t('agentsOverview.toolGroups.noEnabledTools')}; + return {t('agentsOverview.toolGroups.noEnabledTools')}; } return ( -
    +
    {groups.map((group) => ( -
    - {group.label} +
    {group.tools.map((tool) => { const tooltipFields = toolTooltipFields(tool, t); @@ -590,14 +608,14 @@ export const ToolGroupSummary: React.FC = ({ fields={tooltipFields} titleMonospace > - + {tool.name.replace(/_/g, ' ')} - + ); })}
    -
    + ))}
    ); diff --git a/src/web-ui/src/app/scenes/agents/components/_AgentCapabilityGroups.scss b/src/web-ui/src/app/scenes/agents/components/_AgentCapabilityGroups.scss new file mode 100644 index 0000000000..92b361f6dc --- /dev/null +++ b/src/web-ui/src/app/scenes/agents/components/_AgentCapabilityGroups.scss @@ -0,0 +1,86 @@ +// Shared layout for Agent capability configuration; controls own their paint. +@mixin capability-group-layout($kind) { + .#{$kind}-group-picker, + .#{$kind}-group-summary { + display: flex; + min-width: 0; + flex-direction: column; + gap: var(--openbitfun-space-4); + } + + .#{$kind}-group-picker { + &__sections { + display: flex; + flex-direction: column; + gap: var(--openbitfun-space-5); + } + + &__group-head, + &__group-title-wrap, + &__group-actions { + display: flex; + align-items: center; + min-width: 0; + gap: var(--openbitfun-space-2); + } + + &__group-head { + justify-content: space-between; + flex-wrap: wrap; + } + + &__group-title-wrap { + flex: 1 1 auto; + } + + &__group-name { + min-width: 0; + color: var(--openbitfun-color-content-primary); + font-size: var(--openbitfun-type-label-md-font-size); + font-weight: var(--openbitfun-type-label-selected-font-weight); + line-height: var(--openbitfun-type-label-md-line-height); + } + + &__selected-count, + &__group-count { + color: var(--openbitfun-color-content-muted); + font-size: var(--openbitfun-type-meta-font-size); + line-height: var(--openbitfun-type-meta-line-height); + font-variant-numeric: tabular-nums; + white-space: nowrap; + } + + &__group-actions { + flex: 0 0 auto; + } + + &__token-grid { + display: flex; + flex-wrap: wrap; + gap: var(--openbitfun-space-2); + } + + &__token { + max-width: 100%; + } + } + + .#{$kind}-group-summary { + &__#{$kind}s { + display: flex; + min-width: 0; + flex-wrap: wrap; + gap: var(--openbitfun-space-2); + } + + &__item { + max-width: 100%; + } + + &__empty { + color: var(--openbitfun-color-content-muted); + font-size: var(--openbitfun-type-body-sm-font-size); + line-height: var(--openbitfun-type-body-sm-line-height); + } + } +} diff --git a/src/web-ui/src/app/scenes/agents/components/_AgentSurfaceCard.scss b/src/web-ui/src/app/scenes/agents/components/_AgentSurfaceCard.scss deleted file mode 100644 index 6173a363d3..0000000000 --- a/src/web-ui/src/app/scenes/agents/components/_AgentSurfaceCard.scss +++ /dev/null @@ -1,59 +0,0 @@ -@use '../../../styles/surface-stagger'; - -@mixin agent-surface-card() { - width: 100%; - min-width: 0; - height: 148px; - border: 0; - border-radius: var(--openbitfun-layout-field-group-radius); - background: var(--openbitfun-color-surface-tertiary); - display: flex; - flex-direction: column; - position: relative; - overflow: hidden; - cursor: pointer; - animation: agent-surface-card-in 0.28s var(--openbitfun-motion-easing-decelerate) both; - @include surface-stagger.animation-delay(60ms); - transition: - transform var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-decelerate), - border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - background-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - &:hover { - border-color: var(--openbitfun-color-field-border-hover); - background: var(--openbitfun-color-surface-subtle); - } - - &:focus-visible { - outline: var(--openbitfun-focus-width) solid var(--openbitfun-color-focus-ring); - outline-offset: 2px; - } -} - -@mixin agent-surface-footer() { - box-sizing: border-box; - min-height: 34px; - border-radius: var(--openbitfun-radius-base); - background: var(--openbitfun-color-surface-subtle); - overflow: hidden; - position: relative; - z-index: 1; -} - -// Keep the appearance part available without decorative dot artwork. -@mixin agent-icon-dot-field() { - display: none; -} - -@keyframes agent-surface-card-in { - from { - opacity: 0; - transform: translateY(10px) scale(0.98); - } - - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} diff --git a/src/web-ui/src/app/scenes/agents/components/skillGroups.test.ts b/src/web-ui/src/app/scenes/agents/components/skillGroups.test.ts deleted file mode 100644 index 8a4edac15d..0000000000 --- a/src/web-ui/src/app/scenes/agents/components/skillGroups.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import type { GroupableSkill } from './skillGroups'; -import { - createUserSkillGroupsConfig, - normalizeUserSkillGroupsConfig, - resolveSkillGroupSummary, - resolveSkillGroups, - setSkillGroupSelection, -} from './skillGroups'; - -const labels = { - builtin: (groupKey: string) => `builtin:${groupKey}`, - other: 'other', -}; - -const skills: GroupableSkill[] = [ - { key: 'builtin::docs', name: 'Docs', description: '', isBuiltin: true, groupKey: 'office' }, - { key: 'builtin::slides', name: 'Slides', description: '', isBuiltin: true, groupKey: 'office' }, - { key: 'builtin::plan', name: 'Plan', description: '', isBuiltin: true, groupKey: 'planning' }, - { key: 'user::review', name: 'Review', description: '', isBuiltin: false }, -]; - -describe('skillGroups', () => { - it('keeps user groups before backend-owned builtin groups', () => { - const groups = resolveSkillGroups(skills, [{ - id: 'daily', - name: 'Daily work', - skillKeys: ['user::review', 'builtin::docs'], - }], labels); - - expect(groups.map((group) => group.id)).toEqual([ - 'user:daily', - 'builtin:office', - 'builtin:planning', - 'other', - ]); - expect(groups[0].skills.map((skill) => skill.key)).toEqual(['builtin::docs', 'user::review']); - }); - - it('clears overlapping skills from the final selection', () => { - expect(setSkillGroupSelection( - ['builtin::docs', 'builtin::slides', 'user::review'], - ['builtin::docs', 'user::review'], - false, - )).toEqual(['builtin::slides']); - }); - - it('uses personal groups first when summarizing enabled skills', () => { - const groups = resolveSkillGroupSummary(skills, [{ - id: 'daily', - name: 'Daily work', - skillKeys: ['builtin::docs'], - }], ['builtin::docs', 'builtin::slides'], labels); - - expect(groups.map((group) => group.id)).toEqual(['user:daily', 'builtin:office']); - expect(groups[1].skills.map((skill) => skill.key)).toEqual(['builtin::slides']); - }); - - it('retains unavailable keys while normalizing persisted groups', () => { - const config = normalizeUserSkillGroupsConfig({ - version: 1, - groups: [{ - id: 'daily', - name: 'Daily work', - skillKeys: ['builtin::docs', 'missing::skill', 'builtin::docs'], - }], - }); - - expect(config.groups[0].skillKeys).toEqual(['builtin::docs', 'missing::skill']); - expect(createUserSkillGroupsConfig(config.groups)).toEqual(config); - }); -}); diff --git a/src/web-ui/src/app/scenes/agents/components/toolGroups.ts b/src/web-ui/src/app/scenes/agents/components/toolGroups.ts index 3b488c6d24..d7dbc8a05c 100644 --- a/src/web-ui/src/app/scenes/agents/components/toolGroups.ts +++ b/src/web-ui/src/app/scenes/agents/components/toolGroups.ts @@ -1,4 +1,4 @@ -import type { TFunction } from 'i18next'; +import type { UseI18nReturn } from '@/infrastructure/i18n/hooks/useI18n'; import type { UserToolGroup, UserToolGroupsConfig } from '@/infrastructure/config/types'; import type { DynamicToolInfo } from '@/shared/types/agent-api'; import { isUserSelectableToolName } from '@/shared/utils/toolVisibility'; @@ -164,7 +164,7 @@ function dynamicGroupLabel(tool: GroupableTool): string { export function resolveToolGroups( tools: GroupableTool[], userGroups: UserToolGroup[], - t: TFunction<'scenes/agents'>, + t: UseI18nReturn['t'], ): ResolvedToolGroup[] { const selectableTools = activeTools(tools); const toolByName = new Map(selectableTools.map((tool) => [tool.name, tool])); @@ -276,7 +276,7 @@ export function resolveToolGroupSummary( tools: GroupableTool[], userGroups: UserToolGroup[], selectedToolNames: readonly string[], - t: TFunction<'scenes/agents'>, + t: UseI18nReturn['t'], ): ResolvedToolGroup[] { const selected = new Set(normalizeToolNames(selectedToolNames)); const groups = resolveToolGroups(tools, userGroups, t); diff --git a/src/web-ui/src/app/scenes/agents/components/useUserSkillGroups.ts b/src/web-ui/src/app/scenes/agents/components/useUserSkillGroups.ts deleted file mode 100644 index bc047de923..0000000000 --- a/src/web-ui/src/app/scenes/agents/components/useUserSkillGroups.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import { configAPI } from '@/infrastructure/api/service-api/ConfigAPI'; -import type { UserSkillGroup } from '@/infrastructure/config/types'; -import { globalEventBus } from '@/infrastructure/event-bus'; -import { - USER_SKILL_GROUPS_CONFIG_PATH, - createUserSkillGroupsConfig, - normalizeUserSkillGroupsConfig, -} from './skillGroups'; - -const USER_SKILL_GROUPS_UPDATED_EVENT = 'user-skill-groups:updated'; - -export function useUserSkillGroups() { - const [groups, setGroups] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - let active = true; - void configAPI.getConfig(USER_SKILL_GROUPS_CONFIG_PATH) - .then((value) => { - if (active) { - setGroups(normalizeUserSkillGroupsConfig(value).groups); - } - }) - .catch(() => { - if (active) { - setGroups([]); - } - }) - .finally(() => { - if (active) { - setLoading(false); - } - }); - - const handleUpdated = (value: unknown) => { - if (active) { - setGroups(normalizeUserSkillGroupsConfig(value).groups); - } - }; - globalEventBus.on(USER_SKILL_GROUPS_UPDATED_EVENT, handleUpdated); - - return () => { - active = false; - globalEventBus.off(USER_SKILL_GROUPS_UPDATED_EVENT, handleUpdated); - }; - }, []); - - const saveGroups = useCallback(async (nextGroups: UserSkillGroup[]) => { - const next = createUserSkillGroupsConfig(nextGroups); - await configAPI.setConfig(USER_SKILL_GROUPS_CONFIG_PATH, next); - setGroups(next.groups); - globalEventBus.emit(USER_SKILL_GROUPS_UPDATED_EVENT, next); - }, []); - - return { groups, loading, saveGroups }; -} diff --git a/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts b/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts index c7c38cd54a..5e274b2537 100644 --- a/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts +++ b/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts @@ -1,3 +1,5 @@ +import { primaryAgentKind, agentName } from '@/shared/agents/identity'; +import { isPrimaryAgent, isOrdinaryAgent } from '../agentVisibility'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { TFunction } from 'i18next'; import { agentAPI, type ModeInfo } from '@/infrastructure/api/service-api/AgentAPI'; @@ -17,7 +19,7 @@ import { useNotification } from '@/shared/notification-system'; import type { DynamicToolInfo } from '@/shared/types/agent-api'; import type { AgentWithCapabilities } from '../agentsStore'; import { enrichCapabilities } from '../utils'; -import { HIDDEN_AGENT_IDS, isAgentInOverviewZone } from '../agentVisibility'; +import { HIDDEN_AGENT_IDS } from '../agentVisibility'; import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; import { loadDefaultReviewTeamDefinition } from '@/shared/services/reviewTeamService'; import { globalEventBus } from '@/infrastructure/event-bus'; @@ -29,7 +31,7 @@ import { createLogger } from '@/shared/utils/logger'; const toolLog = createLogger('useAgentsList'); export type FilterLevel = 'all' | 'builtin' | 'user' | 'project' | 'external'; -export type FilterType = 'all' | 'mode' | 'subagent'; +export type FilterType = 'all' | 'agent' | 'subagent'; /** * State of the tool catalog load, so the UI can distinguish "host doesn't expose @@ -249,7 +251,7 @@ export function useAgentsList({ try { const [modes, subagents, toolCatalog, configs, reviewTeamDefinition, modelConfigs] = await Promise.all([ - agentAPI.getAvailableModes().catch(() => []), + agentAPI.getAvailableModes({ workspacePath: workspacePath || undefined }).catch(() => []), SubagentAPI.listSubagents({ workspacePath: workspacePath || undefined }).catch(() => []), fetchTools(), configAPI.getAgentProfileConfigs().catch(() => ({})), @@ -312,24 +314,22 @@ export function useAgentsList({ enrichCapabilities({ key: `mode::${mode.id}`, id: mode.id, - name: mode.name, + name: agentName(mode, t), description: mode.description, isReadonly: mode.isReadonly, - isReview: false, toolCount: mode.toolCount, defaultTools: mode.defaultTools ?? [], - defaultEnabled: true, - effectiveEnabled: true, source: mode.source, path: mode.path, model: mode.model, + promptCacheScopeKey: mode.promptCacheScopeKey, configProfileId: mode.configProfileId, configProfileLabel: mode.configProfileLabel, configProfileMemberModeIds: mode.configProfileMemberModeIds, visibleSubagentCount: manageableSubagentsByProfile[mode.configProfileId] ?.filter((subagent) => subagent.effectiveEnabled).length ?? 0, capabilities: [], - agentKind: 'mode', + agentKind: primaryAgentKind(mode), }), ); @@ -338,6 +338,7 @@ export function useAgentsList({ return enrichCapabilities({ ...subagent, + name: agentName(subagent, t), capabilities: [], agentKind: 'subagent', subagentModelOverride: modelOverride, @@ -362,7 +363,7 @@ export function useAgentsList({ setLoading(false); } } - }, [canQueryToolCatalog, workspacePath, renderedPeerDeviceId]); + }, [canQueryToolCatalog, workspacePath, renderedPeerDeviceId, t]); useEffect(() => { void loadAgents(); @@ -380,7 +381,7 @@ export function useAgentsList({ }, [loadAgents]); const getModeProfile = useCallback((agentId: string): ModeProfileEntry | null => { - const agent = allAgents.find((item) => item.id === agentId && item.agentKind === 'mode'); + const agent = allAgents.find((item) => item.id === agentId && isPrimaryAgent(item)); if (!agent) { return null; } @@ -395,7 +396,7 @@ export function useAgentsList({ }, [allAgents, modeProfiles]); const getModeConfig = useCallback((agentId: string): AgentProfileConfigItem | null => { - const agent = allAgents.find((item) => item.id === agentId && item.agentKind === 'mode'); + const agent = allAgents.find((item) => item.id === agentId && isPrimaryAgent(item)); if (!agent) return null; const profileId = agent.configProfileId ?? agentId; @@ -504,8 +505,10 @@ export function useAgentsList({ } catch { // ignore } + return true; } catch { notification.error(t('agentsOverview.skillToggleFailed')); + return false; } }, [getModeProfile, notification, t, workspacePath]); @@ -532,8 +535,10 @@ export function useAgentsList({ } catch { // ignore } + return true; } catch { notification.error(t('agentsOverview.skillToggleFailed')); + return false; } }, [getModeProfile, notification, t, workspacePath]); @@ -563,7 +568,7 @@ export function useAgentsList({ [profile.profileId]: updatedSubagents, })); setAllAgents((prev) => prev.map((agent) => ( - agent.agentKind === 'mode' && (agent.configProfileId ?? agent.id) === profile.profileId + isPrimaryAgent(agent) && (agent.configProfileId ?? agent.id) === profile.profileId ? { ...agent, visibleSubagentCount: updatedSubagents.filter((subagent) => subagent.effectiveEnabled).length, @@ -610,7 +615,7 @@ export function useAgentsList({ } if (filterType !== 'all') { - if (filterType === 'mode' && agent.agentKind !== 'mode') return false; + if (filterType === 'agent' && !isOrdinaryAgent(agent)) return false; if (filterType === 'subagent' && agent.agentKind !== 'subagent') return false; } @@ -623,7 +628,7 @@ export function useAgentsList({ }), [allAgents, filterLevel, filterType, searchQuery]); const overviewAgents = useMemo( - () => allAgents.filter((agent) => isAgentInOverviewZone(agent, hiddenAgentIds)), + () => allAgents.filter((agent) => !hiddenAgentIds.has(agent.id) && agent.agentKind !== 'harness'), [allAgents, hiddenAgentIds], ); @@ -633,7 +638,7 @@ export function useAgentsList({ user: overviewAgents.filter((agent) => resolveAgentSource(agent) === 'user').length, project: overviewAgents.filter((agent) => resolveAgentSource(agent) === 'project').length, external: overviewAgents.filter((agent) => resolveAgentSource(agent) === 'external').length, - mode: overviewAgents.filter((agent) => agent.agentKind === 'mode').length, + agent: overviewAgents.filter(isOrdinaryAgent).length, subagent: overviewAgents.filter((agent) => agent.agentKind === 'subagent').length, }), [overviewAgents]); diff --git a/src/web-ui/src/app/scenes/agents/utils.ts b/src/web-ui/src/app/scenes/agents/utils.ts index 9a892009b7..38f216448b 100644 --- a/src/web-ui/src/app/scenes/agents/utils.ts +++ b/src/web-ui/src/app/scenes/agents/utils.ts @@ -1,3 +1,4 @@ +import { isPrimaryAgent } from './agentVisibility'; import type { TFunction } from 'i18next'; import type { AgentSource } from '@/infrastructure/api/service-api/CustomAgentAPI'; import { @@ -8,7 +9,7 @@ import { } from './agentsStore'; const MODE_DESCRIPTION_KEY_BY_ID: Record = { - agentic: 'Agentic', + standard: 'Standard', cowork: 'Cowork', computeruse: 'ComputerUse', deepresearch: 'DeepResearch', @@ -47,7 +48,8 @@ function getAgentBadge( agentKind?: AgentKind, source?: AgentSource, ): AgentBadgeConfig { - if (agentKind === 'mode') { + if (agentKind === 'harness') return { variant: 'accent', label: 'Agent Harness' }; + if (agentKind === 'agent') { if (source === 'user') { return { variant: 'success', label: t('agentCard.badges.userMode') }; } @@ -71,9 +73,10 @@ function getAgentBadge( function getAgentDescription( t: TFunction<'scenes/agents'>, - agent: Pick, + agent: Pick, ): string { const fallback = agent.description?.trim() || '—'; + if (agent.source && agent.source !== 'builtin') return fallback; const canonicalModeKey = MODE_DESCRIPTION_KEY_BY_ID[agent.id.toLowerCase()]; const candidates = Array.from(new Set([ agent.id, @@ -107,8 +110,8 @@ function enrichCapabilities(agent: AgentWithCapabilities): AgentWithCapabilities } const id = agent.id.toLowerCase(); - if (agent.agentKind === 'mode') { - if (id === 'agentic') return { ...agent, capabilities: [{ category: 'coding', level: 5 }, { category: 'analysis', level: 4 }] }; + if (isPrimaryAgent(agent)) { + if (id === 'standard') return { ...agent, capabilities: [{ category: 'coding', level: 5 }, { category: 'analysis', level: 4 }] }; if (id === 'plan') return { ...agent, capabilities: [{ category: 'analysis', level: 5 }, { category: 'docs', level: 3 }] }; if (id === 'cowork') return { ...agent, capabilities: [{ category: 'analysis', level: 4 }, { category: 'creative', level: 3 }] }; if (id === 'computeruse') return { ...agent, capabilities: [{ category: 'ops', level: 5 }, { category: 'analysis', level: 3 }] }; diff --git a/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.scss b/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.scss index 12c66a44d9..7735099b9a 100644 --- a/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.scss +++ b/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.scss @@ -40,11 +40,6 @@ display: none; } - &__host-card { - min-inline-size: 0; - justify-content: flex-start; - } - &__brand-image { display: block; flex: none; @@ -60,21 +55,6 @@ text-align: center; } - &__sidebar-footer { - align-items: flex-start; - justify-content: flex-start; - gap: var(--openbitfun-space-2); - padding: var(--openbitfun-space-4); - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-meta-font-size); - line-height: var(--openbitfun-type-body-sm-line-height); - - > [data-openbitfun-component='icon'] { - flex: none; - margin-block-start: var(--openbitfun-space-1); - } - } - &__main { grid-area: 1 / 2 / 3 / 3; min-inline-size: 0; @@ -682,8 +662,7 @@ } &__sidebar-title, - &__search, - &__sidebar-footer { + &__search { display: none; } diff --git a/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.tsx b/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.tsx index 32e0c5415c..6ab69380e9 100644 --- a/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.tsx +++ b/src/web-ui/src/app/scenes/ecosystem-compatibility/EcosystemCompatibilityScene.tsx @@ -5,7 +5,6 @@ import { NavigationPanel, NavigationPanelBody, NavigationPanelContent, - NavigationPanelFooter, NavigationPanelHeader, NavigationPanelItem, NavigationPanelSection, @@ -811,18 +810,6 @@ const EcosystemCompatibilityScene: React.FC = () => { }} />
    - @@ -868,11 +855,6 @@ const EcosystemCompatibilityScene: React.FC = () => {
    - - -
    { })).toMatchObject({ sessionId: 'miniapp-customize-builtin-gomoku-1', sessionName: 'Customize Gomoku', - agentType: 'agentic', + agentType: 'Standard', workspacePath: 'D:/workspace/OpenBitFun', sessionKind: 'subagent', config: { diff --git a/src/web-ui/src/app/scenes/miniapps/customization/miniAppCustomizationSession.ts b/src/web-ui/src/app/scenes/miniapps/customization/miniAppCustomizationSession.ts index 851ff07b69..4ac5f65ad0 100644 --- a/src/web-ui/src/app/scenes/miniapps/customization/miniAppCustomizationSession.ts +++ b/src/web-ui/src/app/scenes/miniapps/customization/miniAppCustomizationSession.ts @@ -19,7 +19,7 @@ export function buildMiniAppCustomizationSessionRequest( return { sessionId: input.sessionId, sessionName: input.sessionName, - agentType: 'agentic', + agentType: 'Standard', workspacePath: input.workspacePath, remoteConnectionId: input.remoteConnectionId, remoteSshHost: input.remoteSshHost, @@ -92,7 +92,7 @@ export async function launchMiniAppCustomizationSession(params: { params.prompt, created.sessionId, params.displayMessage, - 'agentic', + 'Standard', undefined, { userMessageMetadata: { diff --git a/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts b/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts index ca246191d4..880e5630a3 100644 --- a/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts +++ b/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts @@ -351,7 +351,7 @@ export function useMiniAppBridge( typeof params.sessionName === 'string' && params.sessionName.trim() ? params.sessionName.trim() : `MiniApp: ${appId}`, - 'agentic', + 'Standard', result.workspacePath, { sessionKind: 'miniapp', diff --git a/src/web-ui/src/app/scenes/panel-view/PanelViewScene.tsx b/src/web-ui/src/app/scenes/panel-view/PanelViewScene.tsx index 52899558ef..8fe137685c 100644 --- a/src/web-ui/src/app/scenes/panel-view/PanelViewScene.tsx +++ b/src/web-ui/src/app/scenes/panel-view/PanelViewScene.tsx @@ -24,7 +24,7 @@ const PanelViewScene: React.FC = ({ workspacePath }) => {
    diff --git a/src/web-ui/src/app/scenes/profile/ProfileScene.scss b/src/web-ui/src/app/scenes/profile/ProfileScene.scss index e030ed7ac2..e2f976a86c 100644 --- a/src/web-ui/src/app/scenes/profile/ProfileScene.scss +++ b/src/web-ui/src/app/scenes/profile/ProfileScene.scss @@ -1,4 +1,5 @@ .openbitfun-profile-scene { + container: assistant-scene / inline-size; display: flex; flex-direction: column; flex: 1; diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantCard.tsx b/src/web-ui/src/app/scenes/profile/views/AssistantCard.tsx index 991ae6524c..597f047aac 100644 --- a/src/web-ui/src/app/scenes/profile/views/AssistantCard.tsx +++ b/src/web-ui/src/app/scenes/profile/views/AssistantCard.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { OverflowText, Button, Icon, IconButton, StatusPill, Tooltip } from '@openbitfun/ui'; +import { OverflowText, Button, Card, Icon, IconButton, StatusPill, Tooltip } from '@openbitfun/ui'; import { useTranslation } from 'react-i18next'; @@ -50,113 +50,115 @@ const AssistantCard: React.FC = ({ role="listitem" style={style} > - - -
    - + + + + + + + {name} + {isPrimary && ( + + {t('nursery.card.primaryBadge')} + + )} + + {vibe ? ( + {vibe} + ) : ( + + {t('nursery.card.noVibe')} + + )} + {creature ? ( + + {creature} + + ) : null} + + + - - {onNewSession ? ( - - ) : null} +
    + - - {onSetPrimary ? ( - - + + {onNewSession ? ( + ) : null} - {onDelete ? ( - - - ) : null} + + {onSetPrimary ? ( + + + ) : null} + + {onDelete ? ( + + + ) : null} + - -
    +
    + ); }; diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantConfigPage.tsx b/src/web-ui/src/app/scenes/profile/views/AssistantConfigPage.tsx index 78cb8c0d59..6b380da9a8 100644 --- a/src/web-ui/src/app/scenes/profile/views/AssistantConfigPage.tsx +++ b/src/web-ui/src/app/scenes/profile/views/AssistantConfigPage.tsx @@ -24,7 +24,7 @@ import AssistantAvatarPicker from './AssistantAvatarPicker'; import AssistantQuickInput from './AssistantQuickInput'; import { useNurseryStore } from '../nurseryStore'; import './NurseryView.scss'; -import { OverflowText, Icon, IconButton, Input, ScrollArea, Textarea, Tooltip } from '@openbitfun/ui'; +import { ActionCard, OverflowText, Icon, IconButton, Input, PageHeader, ScrollArea, Spinner, Textarea, Tooltip } from '@openbitfun/ui'; const log = createLogger('AssistantConfigPage'); @@ -331,27 +331,32 @@ const AssistantConfigPage: React.FC = () => { {/* Persona docs */}
    - {t('nursery.assistant.personaDocsTitle')} +
    {PERSONA_DOC_FILES.map((fileName) => { const selected = personaDoc?.fileName === fileName && rightView === 'personaDoc'; const labelKey = fileName.replace(/\.md$/i, '') as 'SOUL' | 'USER' | 'IDENTITY'; return ( - + } + description={fileName} + onClick={() => openPersonaDoc(fileName)} + > + {t(`nursery.assistant.personaDocs.${labelKey}`)} + +
    ); })}
    @@ -368,7 +373,7 @@ const AssistantConfigPage: React.FC = () => { - +
    )} > @@ -402,33 +407,29 @@ const AssistantConfigPage: React.FC = () => {
    - - } - /> - - {t(`nursery.assistant.personaDocs.${docLabelKey}`)} - - } - /> - + + } + /> + + )} + />
    {error &&

    {t('nursery.assistant.personaDocLoadFailed')}: {error}

    } {loading ? ( -
    +
    ) : usesHybridEditor ? (
    @@ -489,21 +490,26 @@ const AssistantConfigPage: React.FC = () => { data-openbitfun-component="assistant-config-page" data-openbitfun-part="root" > - {/* Top bar — back only */} -
    - - } - /> - -
    +
    + + } + /> + + )} + /> +
    {/* Two-column layout */}
    @@ -530,18 +536,18 @@ const AssistantConfigPage: React.FC = () => { onBlur={commitEdit} onKeyDown={onEditKey} className="acp-left-header__name-input" + aria-label={t('hero.editNameTitle')} /> ) : ( - startEdit('name')} - onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); startEdit('name'); } }} title={t('hero.editNameTitle')} > - {identityName} - + {identityName} + )}
    {editingField === 'creature' ? ( @@ -552,18 +558,18 @@ const AssistantConfigPage: React.FC = () => { onBlur={commitEdit} onKeyDown={onEditKey} className="acp-left-header__meta-input" + aria-label={t('identity.creaturePlaceholderShort')} size="sm" /> ) : ( - startEdit('creature')} - onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); startEdit('creature'); } }} > - {displayIdentity.creature || t('identity.creaturePlaceholderShort')} - + {displayIdentity.creature || t('identity.creaturePlaceholderShort')} + )} {(displayIdentity.creature || displayIdentity.vibe) && ( · @@ -576,18 +582,18 @@ const AssistantConfigPage: React.FC = () => { onBlur={commitEdit} onKeyDown={onEditKey} className="acp-left-header__meta-input" + aria-label={t('identity.vibePlaceholderShort')} size="sm" /> ) : ( - startEdit('vibe')} - onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); startEdit('vibe'); } }} > - {displayIdentity.vibe || t('identity.vibePlaceholderShort')} - + {displayIdentity.vibe || t('identity.vibePlaceholderShort')} + )}
    @@ -599,7 +605,7 @@ const AssistantConfigPage: React.FC = () => { assistantName={identityName} /> -

    {t('nursery.assistant.sessionsSectionTitle')}

    + { /\.assistant-defaults-row\s*\{[\s\S]*&--unavailable\s*\{[\s\S]*opacity:\s*0\.7;/, ); expect(stylesheet).toMatch( - /@media \(max-width: 760px\)[\s\S]*\.assistant-defaults-row__source,[\s\S]*display:\s*none;/, + /@container assistant-catalog \(max-width: 760px\)[\s\S]*\.assistant-defaults-row__source,[\s\S]*display:\s*none;/, ); }); }); diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.scss b/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.scss index ee282032c4..bc99061c35 100644 --- a/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.scss +++ b/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.scss @@ -1,92 +1,32 @@ $assistant-defaults-row-columns: - minmax(280px, 2.4fr) - minmax(108px, 0.82fr) - 96px - 88px - 94px - 98px - 76px; - -.nursery-page--assistant-defaults { - background: var(--openbitfun-color-surface-scene); - color: var(--openbitfun-color-content-primary); -} + minmax(0, 2.4fr) + minmax(88px, 0.82fr) + max-content; .assistant-defaults { display: flex; width: 100%; height: 100%; + min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; - &__header { - flex: 0 0 auto; - padding: clamp(16px, 2vw, 24px) clamp(20px, 3vw, 36px) clamp(14px, 1.6vw, 20px); - border-bottom: 1px solid var(--openbitfun-color-border-subtle); - background: var(--openbitfun-color-surface-scene); - - > p { - max-width: 760px; - margin: var(--openbitfun-space-2) 0 0 34px; - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); - line-height: var(--openbitfun-type-body-lg-line-height); - } - } - - &__title-row { + &__toolbar { display: flex; - min-width: 0; - align-items: center; - gap: var(--openbitfun-space-2); - - h2 { - flex: 0 1 auto; - min-width: 0; - margin: 0; - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-heading-dialog-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - line-height: var(--openbitfun-type-label-md-line-height); - letter-spacing: var(--openbitfun-type-heading-dialog-letter-spacing); - } - } - - &__scope { - display: inline-flex; - min-height: 26px; align-items: center; - padding: 0 var(--openbitfun-space-2); - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-pill); - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); - white-space: nowrap; + justify-content: space-between; + flex-wrap: wrap; + gap: var(--openbitfun-space-3); + margin-top: var(--openbitfun-space-4); } &__header-actions { display: flex; align-items: center; + flex-wrap: wrap; gap: var(--openbitfun-space-3); - margin-left: auto; - } - - &__save { - display: inline-flex; - align-items: center; - gap: 6px; - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-label-sm-font-size); - white-space: nowrap; - - &--saved { - color: var(--openbitfun-color-status-success-content); - } - - &--error { - color: var(--openbitfun-color-status-danger-content); - } + min-width: 0; } &__loading { @@ -97,209 +37,76 @@ $assistant-defaults-row-columns: justify-content: center; gap: var(--openbitfun-space-3); color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-label-sm-font-size); + font-size: var(--openbitfun-type-body-sm-font-size); } &__workspace { - position: relative; - display: grid; + display: flex; + min-width: 0; min-height: 0; flex: 1; - grid-template-columns: minmax(0, 1fr) clamp(280px, 25vw, 340px); + flex-direction: column; overflow: hidden; } &__main { + container: assistant-catalog / inline-size; min-width: 0; min-height: 0; - padding: 0 clamp(20px, 3vw, 32px) var(--openbitfun-space-4); + flex: 1; + padding: 0 clamp(var(--openbitfun-space-4), 3cqi, var(--openbitfun-space-8)) var(--openbitfun-space-4); } &__warning { - display: flex; - align-items: center; - gap: var(--openbitfun-space-2); - margin: var(--openbitfun-space-3) 0 0; - padding: var(--openbitfun-space-2) var(--openbitfun-space-3); - border: 1px solid var(--openbitfun-color-status-warning-border); - border-radius: var(--openbitfun-radius-base); - background: var(--openbitfun-color-status-warning-surface); - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); - - > svg { - flex: 0 0 auto; - color: var(--openbitfun-color-status-warning-emphasis); - } - - span { - flex: 1; - } - + margin-top: var(--openbitfun-space-4); } &__footer { - padding: var(--openbitfun-space-3) 0 0; + padding-top: var(--openbitfun-space-3); color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-label-sm-font-size); + font-size: var(--openbitfun-type-meta-font-size); } } -.assistant-defaults-summary { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - margin: 0; - border-bottom: 1px solid var(--openbitfun-color-border-subtle); - - > div { - position: relative; - display: flex; - min-height: 72px; - flex-direction: column; - justify-content: center; - gap: 4px; - padding: var(--openbitfun-space-3) clamp(14px, 1.8vw, 22px); - - & + div::before { - position: absolute; - top: 30%; - bottom: 30%; - left: 0; - width: 1px; - background: var(--openbitfun-color-border-subtle); - content: ''; - } - } - - span { - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); - } +.assistant-defaults-tabs { + margin-top: var(--openbitfun-space-4); - strong { - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-heading-dialog-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - line-height: var(--openbitfun-type-modifier-leading-none-line-height); - letter-spacing: var(--openbitfun-type-heading-dialog-letter-spacing); + &__control { + max-inline-size: 100%; + flex-wrap: wrap; } -} - -.assistant-defaults-tabs { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 2px; - margin-top: clamp(14px, 1.8vw, 20px); - padding: 3px; - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-lg); - background: var(--openbitfun-color-surface-subtle); - button { - display: flex; - min-width: 0; - min-height: 34px; + &__label { + display: inline-flex; align-items: center; - justify-content: center; gap: var(--openbitfun-space-2); - padding: 0 var(--openbitfun-space-3); - border: 0; - border-radius: calc(var(--openbitfun-radius-lg) - 3px); - background: transparent; - color: var(--openbitfun-color-content-secondary); - font-family:var(--openbitfun-type-body-sm-font-family); - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); - cursor: pointer; - transition: - background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - &:hover:not(.is-active) { - background: var(--openbitfun-color-action-quiet-hover); - color: var(--openbitfun-color-content-primary); - } - - &.is-active { - background: var(--openbitfun-color-content-primary); - color: var(--openbitfun-color-surface-scene); - box-shadow: var(--openbitfun-shadow-xs); - } - - small { - display: inline-flex; - min-width: 20px; - height: 18px; - align-items: center; - justify-content: center; - padding: 0 6px; - border-radius: var(--openbitfun-radius-pill); - background: color-mix(in srgb, currentColor 10%, transparent); - color: inherit; - font-size: var(--openbitfun-type-meta-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - } } } .assistant-defaults-controls { display: flex; align-items: center; - gap: var(--openbitfun-space-2); - padding: clamp(12px, 1.5vw, 16px) 0 var(--openbitfun-space-3); + flex-wrap: wrap; + justify-content: space-between; + gap: var(--openbitfun-space-3); + padding-block: var(--openbitfun-space-4); } .assistant-defaults-search { - width: min(280px, 34%); - min-width: 210px; + width: min(100%, 280px); + min-width: 0; } .assistant-defaults-filters { display: flex; min-width: 0; - align-items: center; - gap: 6px; - overflow-x: auto; - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } - - button { - min-height: 32px; - padding: 0 var(--openbitfun-space-3); - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-base); - background: transparent; - color: var(--openbitfun-color-content-secondary); - font: inherit; - font-size: var(--openbitfun-type-meta-font-size); - white-space: nowrap; - cursor: pointer; - transition: - background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - &:hover:not(.is-active) { - border-color: var(--openbitfun-color-field-border-hover); - color: var(--openbitfun-color-content-primary); - } - - &.is-active { - border-color: var(--openbitfun-color-content-primary); - background: var(--openbitfun-color-content-primary); - color: var(--openbitfun-color-surface-scene); - } - } + flex-wrap: wrap; } .assistant-defaults-list { min-width: 0; - border-top: 1px solid var(--openbitfun-color-border-subtle); - border-bottom: 1px solid var(--openbitfun-color-border-subtle); + border: 1px solid var(--openbitfun-color-border-subtle); + border-radius: var(--openbitfun-radius-lg); &__header, .assistant-defaults-row { @@ -311,9 +118,12 @@ $assistant-defaults-row-columns: &__header { position: sticky; top: 0; - z-index: 4; + z-index: 1; min-height: 36px; + border-start-start-radius: inherit; + border-start-end-radius: inherit; border-bottom: 1px solid var(--openbitfun-color-border-subtle); + // Sticky headers need an opaque surface above the scrolling rows. background: var(--openbitfun-color-surface-scene); color: var(--openbitfun-color-content-muted); font-size: var(--openbitfun-type-meta-font-size); @@ -336,7 +146,6 @@ $assistant-defaults-row-columns: border-bottom: 1px solid var(--openbitfun-color-border-subtle); transition: background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), opacity var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); &:last-child { @@ -348,8 +157,7 @@ $assistant-defaults-row-columns: } &--selected { - background: var(--openbitfun-color-action-quiet-hover); - box-shadow: inset 3px 0 0 var(--openbitfun-color-content-primary); + background: var(--openbitfun-color-selection-surface); } &--unavailable { @@ -360,10 +168,15 @@ $assistant-defaults-row-columns: } } + &__name-cell { + min-width: 0; + } + &__identity { + width: 100%; display: flex; min-width: 0; - min-height: 63px; + min-height: 64px; align-items: center; gap: var(--openbitfun-space-2); padding: var(--openbitfun-space-2) var(--openbitfun-space-3) var(--openbitfun-space-2) var(--openbitfun-space-2); @@ -377,25 +190,15 @@ $assistant-defaults-row-columns: &__icon { display: inline-flex; - width: 32px; - height: 32px; + width: 28px; + height: 28px; flex: 0 0 auto; align-items: center; justify-content: center; - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-base); - background: var(--openbitfun-color-surface-subtle); + border-radius: var(--openbitfun-radius-sm); color: var(--openbitfun-color-content-secondary); } - &__icon--skill { - color: var(--openbitfun-color-accent-secondary); - } - - &__icon--mcp { - color: var(--openbitfun-color-accent-default); - } - &__copy { display: flex; min-width: 0; @@ -445,11 +248,6 @@ $assistant-defaults-row-columns: line-height: var(--openbitfun-type-body-lg-line-height); } - &__difference { - color: var(--openbitfun-color-content-primary); - font-weight: var(--openbitfun-type-label-sm-font-weight); - } - &__actions { display: flex; align-items: center; @@ -458,73 +256,31 @@ $assistant-defaults-row-columns: padding-right: var(--openbitfun-space-2); } - &__detail { + &__toggle { display: inline-flex; - width: 28px; - height: 28px; flex: 0 0 auto; align-items: center; - justify-content: center; - padding: 0; - border: 0; - border-radius: var(--openbitfun-radius-sm); - background: transparent; - color: var(--openbitfun-color-content-muted); - cursor: pointer; - transition: - background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - transform var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); + gap: var(--openbitfun-space-1); - &:hover { - background: var(--openbitfun-color-action-quiet-hover); - color: var(--openbitfun-color-content-primary); + // Reserve the marker slot so toggling never shifts the controls. + &::before { + content: ''; + flex: 0 0 auto; + inline-size: var(--openbitfun-space-1); + block-size: var(--openbitfun-space-1); + border-radius: var(--openbitfun-radius-pill); } - } -} - -.assistant-defaults-state { - display: inline-flex; - align-items: center; - gap: 6px; - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); - white-space: nowrap; - &__dot { - width: 7px; - height: 7px; - flex: 0 0 auto; - border-radius: 50%; - background: var(--openbitfun-color-content-muted); - } - - &--available &__dot { - background: var(--openbitfun-color-status-success-emphasis); + &--changed::before { + background: var(--openbitfun-color-content-muted); + } } - &--unavailable &__dot { - background: var(--openbitfun-color-status-warning-emphasis); + &__detail { + flex: 0 0 auto; } } -.assistant-defaults-badge { - display: inline-flex; - min-height: 24px; - align-items: center; - padding: 0 var(--openbitfun-space-2); - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-base); - background: var(--openbitfun-color-action-quiet-hover); - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-meta-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); - - &--muted { - background: transparent; - color: var(--openbitfun-color-content-muted); - } -} .assistant-defaults-groups { display: flex; @@ -533,40 +289,15 @@ $assistant-defaults-row-columns: .assistant-defaults-group__icon { display: inline-flex; - width: 28px; - height: 28px; flex: 0 0 auto; align-items: center; justify-content: center; - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-base); - background: var(--openbitfun-color-surface-subtle); - color: var(--openbitfun-color-accent-default); + color: var(--openbitfun-color-content-secondary); } .assistant-defaults-group__detail, .assistant-defaults-group__collapse { - display: inline-flex; - width: 28px; - height: 28px; flex: 0 0 auto; - align-items: center; - justify-content: center; - padding: 0; - border: 0; - border-radius: var(--openbitfun-radius-sm); - background: transparent; - color: var(--openbitfun-color-content-muted); - cursor: pointer; - transition: - background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - transform var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - &:hover { - background: var(--openbitfun-color-action-quiet-hover); - color: var(--openbitfun-color-content-primary); - } } .assistant-defaults-group { @@ -576,10 +307,6 @@ $assistant-defaults-row-columns: border-bottom: 0; } - &[data-openbitfun-state~='collapsed'] &__collapse svg { - transform: rotate(-90deg); - } - &__header { display: flex; min-height: 48px; @@ -589,10 +316,6 @@ $assistant-defaults-row-columns: background: var(--openbitfun-color-surface-subtle); } - &__collapse svg { - transition: transform var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - } - &__icon { width: 28px; height: 28px; @@ -637,166 +360,66 @@ $assistant-defaults-row-columns: } } -.assistant-defaults-empty { - display: flex; - min-height: 120px; - align-items: center; - justify-content: center; - flex-direction: column; - gap: var(--openbitfun-space-3); - color: var(--openbitfun-color-content-muted); - text-align: center; - - p { - max-width: 420px; - margin: 0; - font-size: var(--openbitfun-type-label-sm-font-size); - line-height: var(--openbitfun-type-body-lg-line-height); - } - -} - .assistant-defaults-detail { display: flex; min-width: 0; min-height: 0; + flex: 1; flex-direction: column; overflow: hidden; - border-left: 1px solid var(--openbitfun-color-border-subtle); - background: var(--openbitfun-color-surface-scene); - &--empty { - align-items: center; - justify-content: center; + &__header { + flex: 0 0 auto; } - &__empty { + &__body { display: flex; - max-width: 220px; - align-items: center; + flex: 0 0 auto; flex-direction: column; - gap: var(--openbitfun-space-2); - padding: var(--openbitfun-space-5); - color: var(--openbitfun-color-content-muted); - text-align: center; - - > svg { - margin-bottom: var(--openbitfun-space-1); - } - - strong { - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - } - - span { - font-size: var(--openbitfun-type-meta-font-size); - line-height: var(--openbitfun-type-body-lg-line-height); - } + gap: var(--openbitfun-space-6); } - &__header { - display: flex; - min-height: 64px; - flex: 0 0 auto; - align-items: flex-start; - justify-content: space-between; - gap: var(--openbitfun-space-4); - padding: var(--openbitfun-space-4) clamp(16px, 2vw, 22px) var(--openbitfun-space-3); - border-bottom: 1px solid var(--openbitfun-color-border-subtle); - - > div { - min-width: 0; - } - - h3 { - margin: 2px 0 0; - overflow-wrap: anywhere; - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-flow-section-title-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - line-height: var(--openbitfun-type-label-md-line-height); - letter-spacing: var(--openbitfun-type-heading-dialog-letter-spacing); - } - - > button { - display: inline-flex; - width: 26px; - height: 26px; - flex: 0 0 auto; - align-items: center; - justify-content: center; - padding: 0; - border: 0; - border-radius: var(--openbitfun-radius-sm); - background: transparent; - color: var(--openbitfun-color-content-secondary); - cursor: pointer; - - &:hover { - background: var(--openbitfun-color-action-quiet-hover); - color: var(--openbitfun-color-content-primary); - } - } + &__description, + &__permission-hint { + margin: 0; + overflow-wrap: anywhere; + color: var(--openbitfun-color-content-secondary); + font-size: var(--openbitfun-type-body-sm-font-size); + line-height: var(--openbitfun-type-body-sm-line-height); } - &__eyebrow { - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-meta-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - letter-spacing: var(--openbitfun-type-modifier-tracking-widest-letter-spacing); - text-transform: uppercase; + &__description { + white-space: pre-wrap; } - &__body { - min-height: 0; - flex: 1; - padding: 0 clamp(16px, 2vw, 22px) var(--openbitfun-space-5); - - > section { - padding: var(--openbitfun-space-3) 0; - border-top: 1px solid var(--openbitfun-color-border-subtle); - - &:first-of-type { - border-top: 0; - } - - h4 { - margin: 0 0 var(--openbitfun-space-3); - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - } + &__description-scroll { + max-block-size: min(160px, 25vh); + } - dl { - display: flex; - margin: 0; - flex-direction: column; - gap: var(--openbitfun-space-3); - } - } + &__permission-hint { + margin-top: var(--openbitfun-space-2); } - &__description { + &__fields { + display: flex; margin: 0; - padding: var(--openbitfun-space-3) 0; - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); - line-height: var(--openbitfun-type-body-lg-line-height); + flex-direction: column; + gap: var(--openbitfun-space-3); } &__field { display: grid; - grid-template-columns: minmax(80px, 0.68fr) minmax(0, 1fr); - gap: var(--openbitfun-space-3); + grid-template-columns: minmax(0, 0.55fr) minmax(0, 1fr); + gap: var(--openbitfun-space-4); + align-items: baseline; dt, dd { min-width: 0; margin: 0; - font-size: var(--openbitfun-type-meta-font-size); - line-height: var(--openbitfun-type-body-lg-line-height); + overflow-wrap: anywhere; + font-size: var(--openbitfun-type-body-sm-font-size); + line-height: var(--openbitfun-type-body-sm-line-height); } dt { @@ -804,259 +427,53 @@ $assistant-defaults-row-columns: } dd { - overflow-wrap: anywhere; color: var(--openbitfun-color-content-secondary); } } - - &__permission { - padding: var(--openbitfun-space-3); - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-base); - background: var(--openbitfun-color-surface-subtle); - - strong { - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - } - - p { - margin: var(--openbitfun-space-2) 0 0; - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-meta-font-size); - line-height: var(--openbitfun-type-body-lg-line-height); - } - } - - &__management-row { - display: flex; - align-items: center; - gap: var(--openbitfun-space-3); - padding: var(--openbitfun-space-2) 0; - - > div { - display: flex; - min-width: 0; - flex: 1; - flex-direction: column; - gap: 3px; - - strong { - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); - } - - span { - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-meta-font-size); - line-height: var(--openbitfun-type-body-lg-line-height); - } - } - } - - &__note { - margin: 0; - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-label-sm-font-size); - line-height: var(--openbitfun-type-body-lg-line-height); - } } .assistant-defaults-row__identity:focus-visible, -.assistant-defaults-row__detail:focus-visible, -.assistant-defaults-group__collapse:focus-visible, -.assistant-defaults-group__detail:focus-visible, -.assistant-defaults-detail__header > button:focus-visible, -.assistant-defaults-tabs > button:focus-visible, -.assistant-defaults-filters > button:focus-visible { - outline: 2px solid color-mix(in srgb, var(--openbitfun-color-accent-default) 62%, transparent); - outline-offset: 2px; -} - -@media (max-width: 1320px) { - .assistant-defaults-list { - &__header, - .assistant-defaults-row { - grid-template-columns: - minmax(260px, 2fr) - minmax(104px, 0.8fr) - 96px - 88px - 94px - 76px; - } - - &__header > :nth-child(6) { - display: none; - } - } - - .assistant-defaults-row__access { - display: none; - } -} - -@media (max-width: 1200px) { - .assistant-defaults__workspace { - grid-template-columns: minmax(0, 1fr) clamp(260px, 28vw, 310px); - } -} - -@media (max-width: 960px) { - .assistant-defaults { - &__header-actions { - gap: var(--openbitfun-space-3); - } - - &__main { - padding-right: var(--openbitfun-space-5); - padding-left: var(--openbitfun-space-5); - } - } - - .assistant-defaults-controls { - align-items: stretch; - flex-direction: column; - } - - .assistant-defaults-search { - width: 100%; - } - - .assistant-defaults-list { - &__header, - .assistant-defaults-row { - grid-template-columns: - minmax(250px, 2fr) - minmax(100px, 0.8fr) - 92px - 86px - 76px; - } - - &__header > :nth-child(5) { - display: none; - } - } - - .assistant-defaults-row__difference { - display: none; - } +.assistant-defaults-detail__description-scroll:focus-visible { + outline: var(--openbitfun-focus-width) solid var(--openbitfun-color-focus-ring); + outline-offset: calc(-1 * var(--openbitfun-focus-width)); + border-radius: var(--openbitfun-radius-sm); } -@media (max-width: 760px) { - .assistant-defaults { - &__header { - padding: var(--openbitfun-space-4); - - > p { - margin-left: 0; - } - } - - &__title-row { - flex-wrap: wrap; - } - - &__scope { - order: 3; - margin-left: 34px; - } - - &__header-actions { - width: 100%; - margin: var(--openbitfun-space-2) 0 0 34px; - } - } - - .assistant-defaults-summary > div { - min-height: 64px; - padding: var(--openbitfun-space-2); - } - - .assistant-defaults-list { - &__header, - .assistant-defaults-row { - grid-template-columns: minmax(230px, 1fr) 92px 76px; - } - - &__header > :nth-child(2), - &__header > :nth-child(3) { - display: none; - } +// Column density follows the catalog width. +@container assistant-catalog (max-width: 760px) { + .assistant-defaults-list__header, + .assistant-defaults-list .assistant-defaults-row { + grid-template-columns: minmax(0, 1fr) max-content; } .assistant-defaults-row__source, - .assistant-defaults-row__cell:has(.assistant-defaults-state) { - display: none; - } - - .assistant-defaults-group__header > .assistant-defaults-state, - .assistant-defaults-group__count { + .assistant-defaults-list__header > :nth-child(2) { display: none; } } -@media (max-width: 560px) { - .assistant-defaults { - &__header-actions { - align-items: flex-start; - flex-direction: column; - } - - &__reset { - width: 100%; - } - - &__main { - padding-right: var(--openbitfun-space-4); - padding-left: var(--openbitfun-space-4); - } - } - - .assistant-defaults-summary { - strong { - font-size: var(--openbitfun-type-heading-dialog-font-size); - } - - span { - font-size: var(--openbitfun-type-meta-font-size); - } +@container assistant-catalog (max-width: 520px) { + .assistant-defaults-row__icon, + .assistant-defaults-group__status { + display: none; } - .assistant-defaults-tabs button { - min-height: 34px; - padding: 0 var(--openbitfun-space-2); - font-size: var(--openbitfun-type-label-sm-font-size); - - small { - display: none; - } + .assistant-defaults-row__cell { + padding-inline: var(--openbitfun-space-1); } - .assistant-defaults-row { - &__identity { - gap: var(--openbitfun-space-2); - } - - &__icon { - width: 32px; - height: 32px; - } + .assistant-defaults-row__actions { + gap: var(--openbitfun-space-1); + padding-inline-end: var(--openbitfun-space-1); } - .assistant-defaults-detail { + .assistant-defaults-search { width: 100%; } } @media (prefers-reduced-motion: reduce) { - .assistant-defaults *, - .assistant-defaults *::before, - .assistant-defaults *::after { - scroll-behavior: auto !important; - transition-duration: 0.01ms !important; + .assistant-defaults-row { + transition: none; } } diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx b/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx index 4015d090cf..884eb58f1b 100644 --- a/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx +++ b/src/web-ui/src/app/scenes/profile/views/AssistantDefaultsPage.tsx @@ -1,6 +1,34 @@ -import { OverflowText, Button, Icon, IconButton, SearchField, Switch, Tooltip, ScrollArea } from '@openbitfun/ui'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { CircleAlert, Plug2, RotateCcw, Wrench } from 'lucide-react'; +import { + Alert, + Button, + Dialog, + DialogBody, + DialogClose, + DialogDescription, + DialogHeader, + DialogHeaderActions, + DialogHeading, + DialogTitle, + Empty, + Field, + FieldGroup, + FieldRow, + FormSection, + Icon, + IconButton, + NumberBadge, + OverflowText, + PageHeader, + ScrollArea, + SearchField, + SegmentedControl, + Spinner, + StatusPill, + Switch, + Tooltip, +} from '@openbitfun/ui'; +import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; +import { Plug2, RotateCcw, Wrench } from 'lucide-react'; import { configAPI } from '@/infrastructure/api/service-api/ConfigAPI'; import { MCPAPI, type MCPServerInfo } from '@/infrastructure/api/service-api/MCPAPI'; @@ -21,7 +49,6 @@ import { differsFromProductDefault, isMcpServerAvailable, matchesAssistantDefaultsFilter, - summarizeAssistantDefaults, type AssistantDefaultsFilterableItem, type AssistantDefaultsStatusFilter, } from './assistantDefaultsPresentation'; @@ -122,10 +149,12 @@ const AssistantDefaultsPage: React.FC = () => { const [resetting, setResetting] = useState(false); const [saveState, setSaveState] = useState('saved'); const [activeTab, setActiveTab] = useState('skills'); + const categoryPanelId = useId(); const [statusFilter, setStatusFilter] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); const [detail, setDetail] = useState(null); + const [detailOpen, setDetailOpen] = useState(false); const loadRequestIdRef = useRef(0); // Distinguish "host doesn't expose a catalog" / "read failed" / "really no @@ -164,6 +193,8 @@ const AssistantDefaultsPage: React.FC = () => { // See PR #2428 round 6. const requestId = ++loadRequestIdRef.current; setLoading(true); + setDetailOpen(false); + setDetail(null); setLoadWarning(false); try { // Skip the tool catalog invoke when the peer host cannot answer it, @@ -232,15 +263,6 @@ const AssistantDefaultsPage: React.FC = () => { void loadDefaults(); }, [loadDefaults, renderedPeerDeviceId]); - useEffect(() => { - if (!detail) return; - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') setDetail(null); - }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [detail]); - const duplicateSkillNames = useMemo( () => buildDuplicateSkillNameSet(modeSkills), [modeSkills], @@ -371,11 +393,6 @@ const AssistantDefaultsPage: React.FC = () => { () => [...skillRows, ...builtinRows, ...mcpRows], [builtinRows, mcpRows, skillRows], ); - const summary = useMemo( - () => summarizeAssistantDefaults(allCapabilityRows), - [allCapabilityRows], - ); - const mcpGroups = useMemo(() => { const ids = new Set([ ...mcpRows.map((row) => { @@ -537,7 +554,7 @@ const AssistantDefaultsPage: React.FC = () => { const handleTabChange = useCallback((tab: AssistantDefaultsTab) => { setActiveTab(tab); - setDetail(null); + setDetailOpen(false); }, []); const toggleCollapse = useCallback((id: string) => { @@ -550,7 +567,8 @@ const AssistantDefaultsPage: React.FC = () => { }, []); const openDetail = useCallback((nextDetail: TemplateDetail) => { - setDetail((previous) => (isSameDetail(previous, nextDetail) ? null : nextDetail)); + setDetail(nextDetail); + setDetailOpen(true); }, []); const toggleCapability = useCallback((row: CapabilityRow) => { @@ -570,8 +588,12 @@ const AssistantDefaultsPage: React.FC = () => { ], [t]); const renderCapabilityRow = (row: CapabilityRow) => { - const selected = isSameDetail(detail, row.detail); + const selected = detailOpen && isSameDetail(detail, row.detail); const changed = differsFromProductDefault(row); + const defaultState = row.defaultEnabled + ? t('nursery.template.state.enabled') + : t('nursery.template.state.disabled'); + const differenceHint = `${t('nursery.template.summary.changed')} · ${t('nursery.template.detailFields.productDefault')}: ${defaultState}`; const loadingRow = row.detail.type === 'skill' ? skillsLoading[row.detail.skill.key] : row.detail.type === 'tool' && toolsLoading[row.detail.tool.name]; @@ -589,54 +611,45 @@ const AssistantDefaultsPage: React.FC = () => { ].filter(Boolean).join(' ') || undefined; const rowContent = ( <> - -
    {row.source}
    -
    - - - {row.available - ? t('nursery.template.availability.available') - : t('nursery.template.availability.unavailable')} - -
    -
    - - {row.enabled ? t('nursery.template.state.enabled') : t('nursery.template.state.disabled')} - +
    +
    -
    - {changed ? t('nursery.template.valueYes') : t('nursery.template.valueNo')} -
    -
    {row.accessLabel}
    +
    {row.source}
    - toggleCapability(row)} - aria-label={t('nursery.template.toggleCapability', { name: row.name })} - /> - + aria-haspopup="dialog" + icon={} + />
    ); @@ -674,30 +687,28 @@ const AssistantDefaultsPage: React.FC = () => {
    {t('nursery.template.columns.name')}
    {t('nursery.template.columns.source')}
    -
    {t('nursery.template.columns.availability')}
    -
    {t('nursery.template.columns.state')}
    -
    {t('nursery.template.columns.changed')}
    -
    {t('nursery.template.columns.access')}
    ); const renderEmptyState = (message: string) => ( -
    - -

    {message}

    - {(searchQuery || statusFilter !== 'all') ? ( - - ) : null} +
    + } + description={message} + actions={(searchQuery || statusFilter !== 'all') ? ( + + ) : undefined} + />
    ); @@ -736,37 +747,37 @@ const AssistantDefaultsPage: React.FC = () => { data-openbitfun-state={collapsed ? 'collapsed' : undefined} >
    - + icon={} + />
    {serverName} {getMcpStatusLabel(group.server?.status)}
    - - + {available ? t('nursery.template.availability.available') : t('nursery.template.availability.unavailable')} - + {t('nursery.template.enabledCount', { enabled: enabledCount, total: group.allRows.length })} - + aria-haspopup="dialog" + icon={} + /> {allNames.length > 0 ? ( { ); }; + const saveLabel = saveState === 'saving' + ? t('nursery.template.saveState.saving') + : saveState === 'error' + ? t('nursery.template.saveState.error') + : t('nursery.template.saveState.saved'); + + const renderSaveStatus = () => ( + + : } + > + {saveLabel} + + ); + + const renderAvailability = (available: boolean) => ( + + {available + ? t('nursery.template.availability.available') + : t('nursery.template.availability.unavailable')} + + ); + const renderDetailField = (label: string, value: React.ReactNode) => (
    {label}
    @@ -798,23 +836,38 @@ const AssistantDefaultsPage: React.FC = () => {
    ); - const renderDetailPanel = () => { - if (!detail) { - return ( - - ); - } + +
    + + ); + + const renderDetail = () => { + if (!detail) return null; if (detail.type === 'mcpServer') { const server = mcpServers.find((candidate) => candidate.id === detail.serverId); @@ -826,65 +879,49 @@ const AssistantDefaultsPage: React.FC = () => { const allEnabled = allNames.length > 0 && rows.every((row) => row.enabled); const available = isMcpServerAvailable(server); const title = server?.name ?? detail.serverId; - return ( - - ); + + + + ) :

    {t('nursery.template.mcpServerNoTools')}

    } + +
    + {renderDetailField(t('nursery.template.detailFields.fullName'), title)} + {renderDetailField(t('nursery.template.detailFields.originalId'), detail.serverId)} + {renderDetailField(t('nursery.template.detailFields.source'), 'MCP')} + {renderDetailField(t('nursery.template.detailFields.scope'), t('nursery.template.scopeLabel'))} + {renderDetailField(t('nursery.template.detailFields.transport'), server?.transport || '—')} +
    +
    + +
    + {renderDetailField(t('nursery.template.detailFields.currentState'), getMcpStatusLabel(server?.status))} + {renderDetailField(t('nursery.template.detailFields.availability'), renderAvailability(available))} + {renderDetailField(t('nursery.template.detailFields.autoStart'), server?.autoStart ? t('nursery.template.valueYes') : t('nursery.template.valueNo'))} + {renderDetailField(t('nursery.template.detailFields.toolCount'), rows.length)} +
    +
    + + )); } const row = allCapabilityRows.find((candidate) => isSameDetail(candidate.detail, detail)); @@ -895,81 +932,61 @@ const AssistantDefaultsPage: React.FC = () => { : row.kind === 'mcp' ? t('nursery.template.toolTypeMcp') : t('nursery.template.toolTypeBuiltin'); + const saving = detail.type === 'skill' + ? skillsLoading[detail.skill.key] + : toolsLoading[detail.tool.name]; - return ( - - ); + + + + +
    + {renderDetailField(t('nursery.template.detailFields.fullName'), row.name)} + {renderDetailField(t('nursery.template.detailFields.originalId'), row.originalId)} + {renderDetailField(t('nursery.template.detailFields.source'), row.source)} + {renderDetailField(t('nursery.template.detailFields.scope'), t('nursery.template.scopeLabel'))} + {renderDetailField(t('nursery.template.detailFields.productDefault'), row.defaultEnabled + ? t('nursery.template.state.enabled') + : t('nursery.template.state.disabled'))} +
    +
    + +
    + {renderDetailField(t('nursery.template.detailFields.currentState'), row.enabled + ? t('nursery.template.state.enabled') + : t('nursery.template.state.disabled'))} + {renderDetailField(t('nursery.template.detailFields.changed'), changed + ? t('nursery.template.valueYes') + : t('nursery.template.valueNo'))} + {renderDetailField(t('nursery.template.detailFields.availability'), renderAvailability(row.available))} + {row.statusNote ? renderDetailField(t('nursery.template.detailFields.runtimeState'), row.statusNote) : null} +
    +
    + +
    + {row.accessLabel} +

    {row.accessHint}

    +
    +
    + + )); }; const tabs: Array<{ id: AssistantDefaultsTab; label: string; count: number }> = [ @@ -977,36 +994,32 @@ const AssistantDefaultsPage: React.FC = () => { { id: 'builtin', label: t('nursery.template.builtinToolsSection'), count: builtinRows.length }, { id: 'mcp', label: t('nursery.template.mcpToolsSection'), count: mcpRows.length }, ]; - const saveLabel = saveState === 'saving' - ? t('nursery.template.saveState.saving') - : saveState === 'error' - ? t('nursery.template.saveState.error') - : t('nursery.template.saveState.saved'); + const activeCategoryLabel = tabs.find((tab) => tab.id === activeTab)?.label; return (
    -
    -
    - - } - /> - -

    {t('nursery.template.title')}

    - {t('nursery.template.scopeLabel')} +
    + + } + /> + + )} + /> +
    + {t('nursery.template.scopeLabel')}
    - - {saveState === 'saving' - ? - : saveState === 'error' - ? - : } - {saveLabel} - + {renderSaveStatus()}
    -

    {t('nursery.template.pageDescription')}

    {loading ? (
    - + {t('nursery.template.loading')}
    ) : (
    {loadWarning ? ( -
    - - {t('nursery.template.configurationUnavailable')} - -
    + } + onClick={() => void loadDefaults()} + > + {t('nursery.template.retry')} + + )} + /> ) : null} -
    -
    {t('nursery.template.summary.enabled')}{summary.enabled}
    -
    {t('nursery.template.summary.changed')}{summary.changed}
    -
    {t('nursery.template.summary.unavailable')}{summary.unavailable}
    -
    - -
    - {tabs.map((tab) => ( - - ))} +
    + handleTabChange(value as AssistantDefaultsTab)} + options={tabs.map((tab) => ({ + value: tab.id, + label: {tab.label}, + }))} + />
    -
    - setSearchQuery('') : undefined} - clearLabel={searchQuery ? t('nursery.template.clearSearch') : undefined} - leadingIcon={} - placeholder={t('nursery.template.searchPlaceholder')} - aria-label={t('nursery.template.filterLabel')} - /> -
    - {statusFilters.map((filter) => ( - - ))} -
    -
    - -
    tab.id === activeTab)?.label} data-openbitfun-component="assistant-defaults-page" data-openbitfun-part="list"> - {renderListHeader()} - {activeTab === 'mcp' ? renderMcpGroups() : visibleFlatRows.length > 0 ? ( - activeTab === 'skills' ? ( -
    - {visibleFlatRows.map(renderCapabilityRow)} -
    - ) : ( -
    - {visibleFlatRows.map(renderCapabilityRow)} -
    - ) - ) : renderEmptyState(t('nursery.template.noFilterResults'))} -
    -
    {t('nursery.template.resultCount', { count: visibleCount })}
    +
    +
    + setSearchQuery('') : undefined} + clearLabel={searchQuery ? t('nursery.template.clearSearch') : undefined} + leadingIcon={} + placeholder={t('nursery.template.searchPlaceholder')} + aria-label={t('nursery.template.filterLabel')} + /> + setStatusFilter(value as AssistantDefaultsStatusFilter)} + options={statusFilters.map((filter) => ({ value: filter.id, label: filter.label }))} + /> +
    + +
    + {renderListHeader()} + {activeTab === 'mcp' ? renderMcpGroups() : visibleFlatRows.length > 0 ? ( + activeTab === 'skills' ? ( +
    + {visibleFlatRows.map(renderCapabilityRow)} +
    + ) : ( +
    + {visibleFlatRows.map(renderCapabilityRow)} +
    + ) + ) : renderEmptyState(t('nursery.template.noFilterResults'))} +
    +
    {t('nursery.template.resultCount', { count: visibleCount })}
    +
    - {renderDetailPanel()}
    )} + {renderDetail()}
    ); diff --git a/src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.scss b/src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.scss index 332393547b..0b72f10615 100644 --- a/src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.scss +++ b/src/web-ui/src/app/scenes/profile/views/AssistantQuickInput.scss @@ -1,6 +1,6 @@ .aqi { - // Horizontal inset: shadow fade + align with .acp-sessions-area - padding: var(--openbitfun-space-4) var(--openbitfun-space-6) var(--openbitfun-space-5); + // Keep composer and session rows on the same content edge. + padding: var(--openbitfun-space-2) var(--openbitfun-space-1) var(--openbitfun-space-4); &__composer { width: 100%; @@ -26,7 +26,6 @@ &__hint { font-size: var(--openbitfun-type-meta-font-size); color: var(--openbitfun-color-content-muted); - opacity: 0.72; user-select: none; white-space: nowrap; overflow: hidden; diff --git a/src/web-ui/src/app/scenes/profile/views/NurseryGallery.presentation.test.ts b/src/web-ui/src/app/scenes/profile/views/NurseryGallery.presentation.test.ts index b74e4365da..2366a80342 100644 --- a/src/web-ui/src/app/scenes/profile/views/NurseryGallery.presentation.test.ts +++ b/src/web-ui/src/app/scenes/profile/views/NurseryGallery.presentation.test.ts @@ -28,10 +28,12 @@ describe('Nursery gallery presentation', () => { expect(gallerySection).toMatch( /\.nursery-gallery \{\s+background: var\(--openbitfun-color-surface-scene\);/, ); - expect(gallerySection).toContain('background: var(--openbitfun-color-surface-tertiary);'); + expect(readSibling('./NurseryGallery.tsx')).toContain('appearance="subtle"'); + expect(readSibling('./AssistantCard.tsx')).toContain('appearance="subtle"'); + expect(gallerySection).toContain('border-color: var(--openbitfun-color-border-subtle);'); expect(gallerySection).toContain('color: var(--openbitfun-color-content-primary);'); expect(gallerySection).toContain('color: var(--openbitfun-color-content-muted);'); - expect(gallerySection).toContain('border-radius: var(--openbitfun-layout-field-group-radius);'); + expect(gallerySection).toContain('border-radius: var(--openbitfun-layout-card-radius-md);'); expect(gallerySection).not.toContain('--openbitfun-color-content-on-dark'); expect(gallerySection).not.toContain('--openbitfun-color-content-on-light'); expect(gallerySection).not.toContain('--openbitfun-color-overlay-scrim'); diff --git a/src/web-ui/src/app/scenes/profile/views/NurseryGallery.tsx b/src/web-ui/src/app/scenes/profile/views/NurseryGallery.tsx index 7713988961..50992fd4be 100644 --- a/src/web-ui/src/app/scenes/profile/views/NurseryGallery.tsx +++ b/src/web-ui/src/app/scenes/profile/views/NurseryGallery.tsx @@ -1,11 +1,10 @@ -import { Button, Icon } from '@openbitfun/ui'; +import { Button, Card, Icon, NumberBadge, PageHeader, StatusPill } from '@openbitfun/ui'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Bot, CircleAlert, LoaderCircle, Wrench } from 'lucide-react'; import { GalleryEmpty, GalleryLayout, - GalleryPageHeader, GalleryZone, GalleryGrid, GallerySkeleton, @@ -183,10 +182,12 @@ const NurseryGallery: React.FC = () => { data-openbitfun-component="nursery-gallery" data-openbitfun-part="root" > - {
    -
    -
    -

    - {t('nursery.template.title')} -

    - {t('nursery.template.defaultBadge')} -
    -

    {t('nursery.template.subtitle')}

    + +
    +
    +

    + {t('nursery.template.title')} +

    + {t('nursery.template.defaultBadge')} +
    +

    {t('nursery.template.subtitle')}

    -
    - {templateStatsStatus === 'loading' ? ( - <> -
    -
    - + +
    { title={t('nursery.gallery.assistantsTitle')} subtitle={t('nursery.gallery.assistantsSubtitle')} tools={( - {sortedAssistantWorkspacesList.length} + )} > {workspaceLoading && sortedAssistantWorkspacesList.length === 0 ? ( diff --git a/src/web-ui/src/app/scenes/profile/views/NurseryView.scss b/src/web-ui/src/app/scenes/profile/views/NurseryView.scss index c607033cc2..f67a9bc73e 100644 --- a/src/web-ui/src/app/scenes/profile/views/NurseryView.scss +++ b/src/web-ui/src/app/scenes/profile/views/NurseryView.scss @@ -2,7 +2,7 @@ @use '../../../styles/surface-stagger'; // Same as GalleryLayout.scss — nursery sub-pages align with Agents / gallery lists -$nursery-scene-gutter: clamp(40px, 6vw, 80px); +$nursery-scene-gutter: clamp(var(--openbitfun-space-4), 3cqi, var(--openbitfun-space-8)); $nursery-gallery-content-max: 936px; $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); @@ -12,84 +12,41 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); background: var(--openbitfun-color-surface-scene); color: var(--openbitfun-color-content-primary); - .gallery-layout__body { - background: var(--openbitfun-color-surface-scene); - } - - .gallery-page-header, + &__header, .gallery-zones { + box-sizing: border-box; width: min(100%, $nursery-gallery-content-max); - padding-right: $nursery-gallery-gutter; - padding-left: $nursery-gallery-gutter; - } - - .gallery-page-header { - flex-direction: row; - align-items: flex-start; - padding-top: clamp(22px, 2.4vw, 30px); - padding-bottom: clamp(18px, 2vw, 24px); - background: var(--openbitfun-color-surface-scene); - backdrop-filter: none; - -webkit-backdrop-filter: none; - - &__actions { - padding-top: 0; - } - } - - .gallery-zones { - gap: clamp(24px, 2.5vw, 32px); - padding-top: 0; - padding-bottom: clamp(32px, 4vw, 48px); + margin-inline: auto; + padding-inline: $nursery-gallery-gutter; } - .gallery-zone { + &__header { + flex: 0 0 auto; + flex-wrap: wrap; gap: var(--openbitfun-space-4); - - &__title { - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-heading-section-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - letter-spacing: var(--openbitfun-type-heading-dialog-letter-spacing); - } - } - - .gallery-zone__subtitle { - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-label-md-font-size); + padding-block: var(--openbitfun-space-6); } - .gallery-zone-count { - min-width: 36px; - height: 24px; - padding: 0 var(--openbitfun-space-3); - border: 0; - background: var(--openbitfun-color-action-neutral-surface); - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); + .gallery-zones { + gap: var(--openbitfun-space-6); + padding-block: 0 var(--openbitfun-space-8); } &__assistant-grid { align-items: stretch; - grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); - gap: clamp(14px, 1.5vw, 20px); + grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr)); + gap: var(--openbitfun-space-4); } &__assistant-zone { min-width: 0; } - &__skeleton .gallery-skeleton-card { - border-color: var(--openbitfun-color-border-subtle); - background: var(--openbitfun-color-surface-subtle); - } - &__empty { min-height: 180px; - border: 0; - border-radius: var(--openbitfun-layout-field-group-radius); - background: var(--openbitfun-color-surface-tertiary); + border: 1px solid var(--openbitfun-color-border-subtle); + border-radius: var(--openbitfun-layout-card-radius-md); + background: transparent; color: var(--openbitfun-color-content-muted); > span { @@ -107,9 +64,8 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); } small { - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-label-sm-font-size); - line-height: var(--openbitfun-type-body-lg-line-height); + font-size: var(--openbitfun-type-body-sm-font-size); + line-height: var(--openbitfun-type-body-sm-line-height); } } } @@ -117,24 +73,22 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); // ── Assistant defaults ───────────────────────────────────────────────────── .nursery-defaults { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: center; - gap: clamp(20px, 2vw, 28px); min-width: 0; - min-height: 112px; - padding: var(--openbitfun-space-4) var(--openbitfun-space-5); - overflow: hidden; - border: 0; - border-radius: var(--openbitfun-layout-field-group-radius); - background: var(--openbitfun-color-surface-tertiary); - animation: nursery-card-in 0.22s var(--openbitfun-motion-easing-decelerate) both; + animation: nursery-card-in var(--openbitfun-motion-duration-normal) var(--openbitfun-motion-easing-decelerate) both; + + &__surface { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: var(--openbitfun-space-5); + border-color: var(--openbitfun-color-border-subtle); + } &__content { display: flex; min-width: 0; flex-direction: column; - gap: var(--openbitfun-space-1); + gap: var(--openbitfun-space-2); } &__title-row { @@ -148,30 +102,18 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); &__title { margin: 0; color: var(--openbitfun-color-content-primary); + font-family: var(--openbitfun-type-heading-section-font-family); font-size: var(--openbitfun-type-heading-section-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - line-height: var(--openbitfun-type-label-md-line-height); - letter-spacing: var(--openbitfun-type-heading-dialog-letter-spacing); - } - - &__badge { - display: inline-flex; - min-height: 22px; - align-items: center; - padding: 0 var(--openbitfun-space-2); - border-radius: var(--openbitfun-radius-pill); - background: var(--openbitfun-color-action-neutral-surface); - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-meta-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); - white-space: nowrap; + font-weight: var(--openbitfun-type-heading-section-font-weight); + line-height: var(--openbitfun-type-heading-section-line-height); + letter-spacing: var(--openbitfun-type-heading-section-letter-spacing); } &__subtitle { margin: 0; color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-md-font-size); - line-height: var(--openbitfun-type-body-lg-line-height); + font-size: var(--openbitfun-type-body-sm-font-size); + line-height: var(--openbitfun-type-body-sm-line-height); } &__stats { @@ -179,30 +121,18 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); min-height: 20px; align-items: center; gap: var(--openbitfun-space-4); - margin-top: var(--openbitfun-space-1); flex-wrap: wrap; } &__stat { display: inline-flex; align-items: center; - gap: 5px; + gap: var(--openbitfun-space-1); color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); + font-size: var(--openbitfun-type-meta-font-size); white-space: nowrap; - svg { - color: var(--openbitfun-color-content-muted); - } - - & + & { - padding-left: var(--openbitfun-space-4); - border-left: 1px solid var(--openbitfun-color-border-subtle); - } - - &--error, - &--error svg { + &--error { color: var(--openbitfun-color-status-danger-content); } } @@ -229,20 +159,20 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); .assistant-card { display: flex; - width: 100%; min-width: 0; min-height: 148px; - flex-direction: column; - overflow: hidden; - border: 0; - border-radius: var(--openbitfun-layout-field-group-radius); - background: var(--openbitfun-color-surface-tertiary); - animation: nursery-card-in 0.22s var(--openbitfun-motion-easing-decelerate) both; + animation: nursery-card-in var(--openbitfun-motion-duration-normal) var(--openbitfun-motion-easing-decelerate) both; @include surface-stagger.animation-delay(40ms); + &--busy { opacity: var(--openbitfun-opacity-disabled); } + &__surface { + flex: 1; + border-color: var(--openbitfun-color-border-subtle); + } + &__main { display: flex; min-width: 0; @@ -255,8 +185,9 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); background: transparent; color: inherit; font-family: var(--openbitfun-type-body-sm-font-family); - text-align: left; + text-align: start; cursor: pointer; + transition: background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); &:hover:not(:disabled) { background: var(--openbitfun-color-action-neutral-surface-hover); @@ -264,7 +195,7 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); &:focus-visible { outline: var(--openbitfun-focus-width) solid var(--openbitfun-color-focus-ring); - outline-offset: -2px; + outline-offset: calc(-1 * var(--openbitfun-focus-width)); } &:disabled { @@ -272,8 +203,6 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); } } - // ── Header ──────────────────────────────────────────────────────────── - &__header { display: flex; min-width: 0; @@ -283,25 +212,7 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); &__avatar { display: flex; - width: 44px; - height: 44px; - flex-shrink: 0; - align-items: center; - justify-content: center; - border-radius: 50%; - } - - &__emoji { - max-width: 100%; - overflow: hidden; - font-size: var(--openbitfun-type-heading-dialog-font-size); - line-height: var(--openbitfun-type-modifier-leading-none-line-height); - white-space: nowrap; - } - - &__avatar-icon { - flex-shrink: 0; - color: var(--openbitfun-color-content-secondary); + flex: 0 0 auto; } &__header-info { @@ -309,12 +220,13 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); min-width: 0; flex: 1; flex-direction: column; - gap: 2px; + gap: var(--openbitfun-space-1); } &__title-row { display: flex; align-items: center; + gap: var(--openbitfun-space-2); min-width: 0; } @@ -324,35 +236,26 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); font-weight: var(--openbitfun-type-label-selected-font-weight); color: var(--openbitfun-color-content-primary); line-height: var(--openbitfun-type-body-sm-line-height); - white-space: nowrap; - overflow: hidden; } &__badges { display: flex; - gap: 4px; + min-width: 0; + gap: var(--openbitfun-space-1); flex-wrap: wrap; } &__chevron { - flex-shrink: 0; - align-self: flex-start; - margin-left: auto; + flex: 0 0 auto; color: var(--openbitfun-color-content-muted); - transition: color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); } &__vibe { - display: -webkit-box; - max-width: 100%; + min-width: 0; color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); + font-size: var(--openbitfun-type-body-sm-font-size); font-weight: var(--openbitfun-type-body-sm-font-weight); - line-height: var(--openbitfun-type-body-lg-line-height); - overflow: hidden; - word-break: break-word; - -webkit-box-orient: vertical; - -webkit-line-clamp: 1; + line-height: var(--openbitfun-type-body-sm-line-height); &--empty { color: var(--openbitfun-color-content-muted); @@ -364,35 +267,25 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); } &__primary-badge { - margin-left: var(--openbitfun-space-2); - padding: 2px 6px; - flex-shrink: 0; - border-radius: var(--openbitfun-radius-sm); - background: var(--openbitfun-color-action-neutral-surface); - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-meta-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); - line-height: var(--openbitfun-type-modifier-leading-dense-line-height); - white-space: nowrap; + display: inline-flex; + flex: 0 0 auto; } &__footer { display: flex; min-height: 52px; - flex-shrink: 0; + flex: 0 0 auto; align-items: center; justify-content: space-between; - gap: 8px; + gap: var(--openbitfun-space-2); flex-wrap: wrap; padding: var(--openbitfun-space-3) var(--openbitfun-space-5); border-top: 1px solid var(--openbitfun-color-border-subtle); - background: var(--openbitfun-color-surface-tertiary); } &__session-actions { display: inline-flex; min-width: 0; - flex-shrink: 0; align-items: center; gap: var(--openbitfun-space-1); flex-wrap: wrap; @@ -404,7 +297,6 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); align-items: center; gap: var(--openbitfun-space-1); } - } // ── Sub-page chrome (shared by AssistantDefaultsPage / AssistantConfigPage) ─── @@ -413,9 +305,23 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); display: flex; flex-direction: column; height: 100%; + min-width: 0; + min-height: 0; + background: var(--openbitfun-color-surface-scene); + color: var(--openbitfun-color-content-primary); overflow: hidden; animation: nursery-page-in 0.22s var(--openbitfun-motion-easing-decelerate) both; + &__header { + flex: 0 0 auto; + padding: var(--openbitfun-space-5) $nursery-scene-gutter; + border-bottom: 1px solid var(--openbitfun-color-border-subtle); + } + + &__heading { + gap: var(--openbitfun-space-3); + } + // ── Top bar (back icon only, left) ───────────────────────────────────── &__bar { @@ -976,149 +882,49 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); // ── Responsive ──────────────────────────────────────────────────────────── -@media (max-width: 820px) { - .nursery-defaults { - grid-template-columns: 1fr; - - &__action { - grid-column: 1; - justify-self: start; - } - } -} - -@media (max-width: 720px) { - .nursery-model-grid { - grid-template-columns: 1fr; - } - - // Match GalleryLayout narrow gutters (bar stays flush left) - .nursery-page__head-row, - .nursery-page__content { +@container assistant-scene (max-width: 680px) { + .nursery-gallery__header, + .nursery-gallery .gallery-zones { padding-inline: var(--openbitfun-space-4); } - .nursery-tabs .openbitfun-tabs__nav { - padding-inline: var(--openbitfun-space-4); - } - - .nursery-tab-content { - padding-inline: var(--openbitfun-space-4); - } - - .nursery-gallery { - .gallery-page-header, - .gallery-zones { - padding-right: var(--openbitfun-space-4); - padding-left: var(--openbitfun-space-4); - } - - .gallery-zone__header { - flex-direction: row; - align-items: flex-start; - } - - .gallery-zone__tools { - width: auto; - margin-left: auto; - justify-content: flex-end; - } - - &__assistant-grid { - grid-template-columns: 1fr; - } - } - - .nursery-defaults { - grid-template-columns: 1fr; + .nursery-defaults__surface { + grid-template-columns: minmax(0, 1fr); gap: var(--openbitfun-space-4); - min-height: 0; - padding: var(--openbitfun-space-4); - - &__action { - width: 100%; - grid-column: 1; - } - } - - .assistant-card { - min-height: 148px; - - &__main { - padding: var(--openbitfun-space-3) 14px; - } - - &__footer { - padding-inline: var(--openbitfun-space-3); - } } -} - -@media (max-width: 520px) { - .nursery-gallery { - .gallery-page-header { - flex-direction: column; - - &__actions, - &__actions > * { - width: 100%; - } - } - .gallery-zone__header { - gap: var(--openbitfun-space-2); - } + .nursery-defaults__action { + justify-self: start; } - .assistant-card { - &__main, - &__footer { - padding-inline: var(--openbitfun-space-3); - } + .assistant-card__main, + .assistant-card__footer { + padding-inline: var(--openbitfun-space-4); } } // ── Top bar ───────────────────────────────────────────────────────────────── -.acp-page__bar { - display: flex; - align-items: center; - gap: var(--openbitfun-space-2); - padding: 3px var(--openbitfun-space-2); - flex-shrink: 0; - min-height: 32px; -} - -// ── Two-column layout ──────────────────────────────────────────────────────── - .acp-layout { + box-sizing: border-box; flex: 1; + width: min(100%, 1280px); + min-width: 0; min-height: 0; - display: flex; + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(300px, 1fr); overflow: hidden; - padding: 0 clamp( - calc(var(--openbitfun-space-8) + var(--openbitfun-space-1)), - 5.5vw, - calc(var(--openbitfun-space-16) + var(--openbitfun-space-2)) - ); - gap: clamp(var(--openbitfun-space-5), 2.2vw, var(--openbitfun-space-8)); - - &__left { - flex: 0 0 60%; - min-width: 0; - display: flex; - flex-direction: column; - overflow: hidden; - padding: 0; - } + margin-inline: auto; + padding: var(--openbitfun-space-6) $nursery-scene-gutter; + gap: var(--openbitfun-space-6); + &__left, &__right { - flex: 0 0 40%; min-width: 0; + min-height: 0; display: flex; flex-direction: column; overflow: hidden; - padding: 0; } } @@ -1126,15 +932,10 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); .acp-left-header { display: flex; - align-items: flex-start; - gap: var(--openbitfun-space-3); - // Match .aqi and .acp-sessions-area horizontal inset. - padding: clamp( - var(--openbitfun-space-10), - 4.5vh, - calc(var(--openbitfun-space-12) + var(--openbitfun-space-1)) - ) var(--openbitfun-space-6) var(--openbitfun-space-5); - flex-shrink: 0; + align-items: center; + gap: var(--openbitfun-space-4); + padding: var(--openbitfun-space-2) var(--openbitfun-space-1) var(--openbitfun-space-4); + flex: 0 0 auto; &__info { flex: 1; @@ -1144,27 +945,42 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); gap: var(--openbitfun-space-2); } - &__name { - font-size: var(--openbitfun-type-display-sm-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); + &__name, + &__meta-tag { + display: flex; + align-items: center; + min-width: 0; + max-width: 100%; + margin: 0; + padding: var(--openbitfun-space-1); + border: 0; + border-radius: var(--openbitfun-radius-sm); + background: transparent; color: var(--openbitfun-color-content-primary); - letter-spacing: var(--openbitfun-type-heading-dialog-letter-spacing); - line-height: var(--openbitfun-type-display-sm-line-height); + font: inherit; + text-align: start; cursor: pointer; - border-radius: var(--openbitfun-radius-sm); - padding: 1px 4px; - margin-left: -4px; - transition: color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); + transition: background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); &:hover { - color: var(--openbitfun-color-accent-default); + background: var(--openbitfun-color-action-quiet-hover); + } + + &:focus-visible { + outline: var(--openbitfun-focus-width) solid var(--openbitfun-color-focus-ring); + outline-offset: var(--openbitfun-focus-offset); } } + &__name { + align-self: flex-start; + font-size: var(--openbitfun-type-heading-page-font-size); + font-weight: var(--openbitfun-type-heading-page-font-weight); + line-height: var(--openbitfun-type-heading-page-line-height); + letter-spacing: var(--openbitfun-type-heading-page-letter-spacing); + } + &__name-input { - font-size: var(--openbitfun-type-display-sm-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - letter-spacing: var(--openbitfun-type-heading-dialog-letter-spacing); width: 100%; } @@ -1172,40 +988,28 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); display: flex; align-items: center; flex-wrap: wrap; - gap: var(--openbitfun-space-2); + gap: var(--openbitfun-space-1); min-width: 0; - margin-top: 2px; } &__meta-tag { - font-size: var(--openbitfun-type-body-md-font-size); - color: var(--openbitfun-color-content-muted); - cursor: pointer; - border-radius: var(--openbitfun-radius-sm); - padding: 1px 4px; - margin-left: -4px; - transition: color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - &:hover { - color: var(--openbitfun-color-content-secondary); - } + color: var(--openbitfun-color-content-secondary); + font-size: var(--openbitfun-type-body-sm-font-size); + line-height: var(--openbitfun-type-body-sm-line-height); &.is-empty { - font-style: italic; - opacity: 0.45; + color: var(--openbitfun-color-content-muted); } } &__meta-dot { - font-size: var(--openbitfun-type-body-md-font-size); color: var(--openbitfun-color-content-muted); - opacity: 0.35; user-select: none; } &__meta-input { - font-size: var(--openbitfun-type-body-md-font-size); - max-width: 140px; + min-width: 0; + max-width: 220px; } } @@ -1240,7 +1044,7 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); } &:focus-visible { - outline: 2px solid color-mix(in srgb, var(--openbitfun-color-accent-default) 58%, transparent); + outline: var(--openbitfun-focus-width) solid var(--openbitfun-color-focus-ring); outline-offset: 2px; } } @@ -1356,7 +1160,7 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); } &:focus-visible { - outline: 2px solid color-mix(in srgb, var(--openbitfun-color-accent-default) 58%, transparent); + outline: var(--openbitfun-focus-width) solid var(--openbitfun-color-focus-ring); outline-offset: 1px; } @@ -1420,16 +1224,10 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); flex: 1; min-height: 0; overflow-x: hidden; - padding: var(--openbitfun-space-3) var(--openbitfun-space-6) var(--openbitfun-space-5); + padding: var(--openbitfun-space-4) var(--openbitfun-space-1) var(--openbitfun-space-2); &__title { - margin: 0 0 var(--openbitfun-space-2); - padding: 0; - font-size: var(--openbitfun-type-label-md-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - line-height: var(--openbitfun-type-label-md-line-height); - letter-spacing: var(--openbitfun-type-flow-title-letter-spacing); - color: var(--openbitfun-color-content-secondary); + margin-bottom: var(--openbitfun-space-3); } // Override SessionsSection's built-in NavPanel indentation so rows @@ -1471,42 +1269,23 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); overflow: hidden; display: flex; flex-direction: column; - gap: 0; - // Left/right padding comes from parent .acp-layout__right - padding: clamp( - var(--openbitfun-space-10), - 4.5vh, - calc(var(--openbitfun-space-12) + var(--openbitfun-space-1)) - ) 0 clamp(var(--openbitfun-space-10), 6vh, var(--openbitfun-space-12)); } -// Single rounded frame for persona + schedule (see AssistantConfigPage) .acp-right-shell { flex: 1; min-height: 0; display: flex; flex-direction: column; border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-xl); + border-radius: var(--openbitfun-layout-card-radius-md); overflow: hidden; background: transparent; } -// Clear boundary between persona block and schedule block (no panel fill) .acp-right-shell__divider { - flex-shrink: 0; - margin: 0 0 var(--openbitfun-space-3) 0; - border: 0; - height: 0; - border-top: 1px solid var(--openbitfun-color-field-border-hover); -} - -.acp-right-shell--editor .acp-persona-editor { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - padding-top: 0; + flex: 0 0 auto; + height: 1px; + background: var(--openbitfun-color-border-subtle); } // ── Identity card ──────────────────────────────────────────────────────────── @@ -1689,7 +1468,7 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); display: flex; align-items: center; gap: var(--openbitfun-space-2); - padding: var(--openbitfun-space-3) var(--openbitfun-space-4); + padding: var(--openbitfun-space-4); background: transparent; } @@ -1727,57 +1506,15 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); .acp-persona-doc-list { display: flex; flex-direction: column; - gap: var(--openbitfun-space-1); - padding-bottom: var(--openbitfun-space-4); + gap: var(--openbitfun-space-2); + padding: 0 var(--openbitfun-space-4) var(--openbitfun-space-4); } .acp-persona-doc-row { - display: flex; - align-items: center; - gap: var(--openbitfun-space-3); - width: 100%; - min-height: 44px; - padding: var(--openbitfun-space-2) var(--openbitfun-space-3); - margin: 0; - border: none; - border-radius: var(--openbitfun-radius-base); - background: transparent; - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-label-md-font-size); - text-align: left; - cursor: pointer; - transition: - background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - &:hover { - background: var(--openbitfun-color-action-quiet-hover); - } - - &--selected { - background: color-mix(in srgb, var(--openbitfun-color-action-quiet-hover) 70%, transparent); - box-shadow: inset 3px 0 0 var(--openbitfun-color-accent-default); - } - - &__icon { - flex-shrink: 0; - display: inline-flex; - color: var(--openbitfun-color-content-muted); - } - - &__label { - flex: 1; - min-width: 0; - font-weight: var(--openbitfun-type-label-sm-font-weight); - overflow: hidden; - white-space: nowrap; - } + min-width: 0; - &__file { - flex-shrink: 0; - font-size: var(--openbitfun-type-meta-font-size); - font-family: var(--openbitfun-type-code-md-font-family); - color: var(--openbitfun-color-content-muted); + &__control { + width: 100%; } } @@ -1786,7 +1523,6 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); .acp-empty { font-size: var(--openbitfun-type-label-md-font-size); color: var(--openbitfun-color-content-muted); - font-style: italic; padding: var(--openbitfun-space-4) 0; line-height: var(--openbitfun-type-body-lg-line-height); } @@ -1807,37 +1543,16 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); display: flex; flex-direction: column; overflow: hidden; - padding-top: clamp( - var(--openbitfun-space-10), - 4.5vh, - calc(var(--openbitfun-space-12) + var(--openbitfun-space-1)) - ); - animation: nursery-page-in 0.18s var(--openbitfun-motion-easing-decelerate) both; &__head { - flex-shrink: 0; - display: flex; - align-items: center; - gap: var(--openbitfun-space-3); - padding: var(--openbitfun-space-3) var(--openbitfun-space-4); - background: transparent; - } - - &__back.icon-btn { - flex-shrink: 0; - } - - &__title { - flex: 1; - font-size: var(--openbitfun-type-label-md-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - color: var(--openbitfun-color-content-primary); - overflow: hidden; - white-space: nowrap; + flex: 0 0 auto; + padding: var(--openbitfun-space-4); + border-bottom: 1px solid var(--openbitfun-color-border-subtle); } - &__close.icon-btn { - flex-shrink: 0; + &__heading { + width: 100%; + gap: var(--openbitfun-space-3); } &__body { @@ -1932,19 +1647,33 @@ $nursery-gallery-gutter: clamp(16px, 2.5vw, 28px); // ── Responsive ──────────────────────────────────────────────────────────────── -@media (max-width: 860px) { +@container assistant-scene (max-width: 820px) { .acp-layout { - flex-direction: column; - gap: var(--openbitfun-space-6); - - &__left { - flex: none; - max-height: 55vh; - } - + grid-template-columns: minmax(0, 1fr); + grid-template-rows: max-content max-content; + align-content: start; + overflow-y: auto; + gap: var(--openbitfun-space-5); + padding-block: var(--openbitfun-space-4); + + &__left, &__right { - flex: 1; - min-height: 200px; + overflow: visible; } } + + .acp-sessions-area { + flex: none; + max-height: 320px; + } + + .acp-right-info { + flex: none; + min-height: 320px; + } + + .acp-right-shell--editor { + height: 480px; + flex: none; + } } diff --git a/src/web-ui/src/app/scenes/profile/views/assistantDefaultsPresentation.test.ts b/src/web-ui/src/app/scenes/profile/views/assistantDefaultsPresentation.test.ts index 41847e1477..274b6ce813 100644 --- a/src/web-ui/src/app/scenes/profile/views/assistantDefaultsPresentation.test.ts +++ b/src/web-ui/src/app/scenes/profile/views/assistantDefaultsPresentation.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'; import { isMcpServerAvailable, matchesAssistantDefaultsFilter, - summarizeAssistantDefaults, type AssistantDefaultsFilterableItem, } from './assistantDefaultsPresentation'; @@ -28,14 +27,6 @@ const items: AssistantDefaultsFilterableItem[] = [ ]; describe('assistant defaults presentation', () => { - it('summarizes enabled, changed, and unavailable capabilities independently', () => { - expect(summarizeAssistantDefaults(items)).toEqual({ - enabled: 1, - changed: 1, - unavailable: 1, - }); - }); - it('matches names, descriptions, sources, and original ids', () => { expect(matchesAssistantDefaultsFilter(items[0], 'all', 'structured')).toBe(true); expect(matchesAssistantDefaultsFilter(items[0], 'all', 'openbitfun.web')).toBe(true); diff --git a/src/web-ui/src/app/scenes/profile/views/assistantDefaultsPresentation.ts b/src/web-ui/src/app/scenes/profile/views/assistantDefaultsPresentation.ts index 68435b5f7d..d53dc847cf 100644 --- a/src/web-ui/src/app/scenes/profile/views/assistantDefaultsPresentation.ts +++ b/src/web-ui/src/app/scenes/profile/views/assistantDefaultsPresentation.ts @@ -15,12 +15,6 @@ export interface AssistantDefaultsFilterableItem { available: boolean; } -export interface AssistantDefaultsSummary { - enabled: number; - changed: number; - unavailable: number; -} - export interface McpAvailabilityInput { enabled?: boolean; status?: string; @@ -69,14 +63,3 @@ export function matchesAssistantDefaultsFilter( return true; } } - -export function summarizeAssistantDefaults( - items: AssistantDefaultsFilterableItem[], -): AssistantDefaultsSummary { - return items.reduce((summary, item) => { - if (item.enabled) summary.enabled += 1; - if (differsFromProductDefault(item)) summary.changed += 1; - if (!item.available) summary.unavailable += 1; - return summary; - }, { enabled: 0, changed: 0, unavailable: 0 }); -} diff --git a/src/web-ui/src/app/scenes/session/AuxPane.tsx b/src/web-ui/src/app/scenes/session/AuxPane.tsx index fea29e2cd3..fbd604d32c 100644 --- a/src/web-ui/src/app/scenes/session/AuxPane.tsx +++ b/src/web-ui/src/app/scenes/session/AuxPane.tsx @@ -7,6 +7,9 @@ import { forwardRef, useEffect, useRef, useImperativeHandle, useCallback } from 'react'; import { ContentCanvas, useCanvasStore } from '../../components/panels/content-canvas'; +import { usePanelTabCoordinator } from '../../components/panels/content-canvas/hooks/usePanelTabCoordinator'; +import { TAB_EVENTS } from '../../components/panels/content-canvas/types'; +import { collapseSessionAuxPane, expandSessionAuxPane } from './sessionPanelLayout'; import { switchAgentCanvasWorkspace, removeAgentCanvasSnapshot, @@ -50,6 +53,16 @@ const AuxPane = forwardRef( const closeAllTabs = useCanvasStore(state => state.closeAllTabs); const primaryGroup = useCanvasStore(state => state.primaryGroup); const secondaryGroup = useCanvasStore(state => state.secondaryGroup); + const tertiaryGroup = useCanvasStore(state => state.tertiaryGroup); + const canvasWorkspaceKey = useCanvasStore(state => state.workspaceKey); + const { expandPanel, collapsePanel } = usePanelTabCoordinator({ + visibleTabCount: [primaryGroup, secondaryGroup, tertiaryGroup] + .reduce((count, group) => count + group.tabs.filter(tab => !tab.isHidden).length, 0), + scopeKey: canvasWorkspaceKey, + expandEventName: TAB_EVENTS.EXPAND_RIGHT_PANEL, + onExpand: expandSessionAuxPane, + onCollapse: collapseSessionAuxPane, + }); const convertContent = useCallback((oldContent: OldPanelContent): PanelContent => { return { @@ -63,15 +76,15 @@ const AuxPane = forwardRef( useImperativeHandle(ref, () => ({ addTab: (content: OldPanelContent) => { addTab(convertContent(content), 'active'); - window.dispatchEvent(new CustomEvent('expand-right-panel')); + expandPanel(); }, switchToTab: (tabId: string) => { if (primaryGroup.tabs.find(t => t.id === tabId)) { switchToTab(tabId, 'primary'); - window.dispatchEvent(new CustomEvent('expand-right-panel')); + expandPanel(); } else if (secondaryGroup.tabs.find(t => t.id === tabId)) { switchToTab(tabId, 'secondary'); - window.dispatchEvent(new CustomEvent('expand-right-panel')); + expandPanel(); } }, findTabByMetadata: (metadata: Record) => { @@ -97,6 +110,7 @@ const AuxPane = forwardRef( primaryGroup.tabs, secondaryGroup.tabs, convertContent, + expandPanel, ]); const prevWorkspaceIdRef = useRef(undefined); @@ -149,6 +163,8 @@ const AuxPane = forwardRef( workspacePath={workspacePath} mode="agent" isSceneActive={isSceneActive} + onReveal={expandPanel} + onCollapsePanel={collapsePanel} onInteraction={handleInteraction} onBeforeClose={handleBeforeClose} terminalResizeSuspended={terminalResizeSuspended} diff --git a/src/web-ui/src/app/scenes/session/AuxPane.workspace-switch.test.tsx b/src/web-ui/src/app/scenes/session/AuxPane.workspace-switch.test.tsx index 1c05732e9a..1a7123d96d 100644 --- a/src/web-ui/src/app/scenes/session/AuxPane.workspace-switch.test.tsx +++ b/src/web-ui/src/app/scenes/session/AuxPane.workspace-switch.test.tsx @@ -20,6 +20,7 @@ const mocks = vi.hoisted(() => ({ closeAllTabs: vi.fn(), primaryGroup: { tabs: [] }, secondaryGroup: { tabs: [] }, + tertiaryGroup: { tabs: [] }, }, })); @@ -62,6 +63,11 @@ vi.mock('@/shared/utils/logger', () => ({ import AuxPane from './AuxPane'; +vi.mock('./sessionPanelLayout', () => ({ + expandSessionAuxPane: vi.fn(), + collapseSessionAuxPane: vi.fn(), +})); + globalThis.IS_REACT_ACT_ENVIRONMENT = true; describe('AuxPane workspace canvas switching', () => { diff --git a/src/web-ui/src/app/scenes/session/BottomTerminalPane.tsx b/src/web-ui/src/app/scenes/session/BottomTerminalPane.tsx index 1c4a69220e..39ed146e31 100644 --- a/src/web-ui/src/app/scenes/session/BottomTerminalPane.tsx +++ b/src/web-ui/src/app/scenes/session/BottomTerminalPane.tsx @@ -3,13 +3,14 @@ import { CanvasStoreModeContext, ContentCanvas, } from '../../components/panels/content-canvas'; +import { useBottomTerminalCanvasStore } from '../../components/panels/content-canvas/stores'; import { TAB_EVENTS } from '../../components/panels/content-canvas/types'; +import { usePanelTabCoordinator } from '../../components/panels/content-canvas/hooks/usePanelTabCoordinator'; import './BottomTerminalPane.scss'; interface BottomTerminalPaneProps { workspacePath?: string; isSceneActive?: boolean; - isCollapsed: boolean; onExpand: () => void; onCollapse: () => void; terminalResizeSuspended?: boolean; @@ -18,11 +19,20 @@ interface BottomTerminalPaneProps { const BottomTerminalPane: React.FC = ({ workspacePath, isSceneActive = true, - isCollapsed, onExpand, onCollapse, terminalResizeSuspended = false, }) => { + const visibleTabCount = useBottomTerminalCanvasStore(state => ( + [state.primaryGroup, state.secondaryGroup, state.tertiaryGroup] + .reduce((count, group) => count + group.tabs.filter(tab => !tab.isHidden).length, 0) + )); + const { expandPanel, collapsePanel } = usePanelTabCoordinator({ + visibleTabCount, + expandEventName: TAB_EVENTS.EXPAND_BOTTOM_TERMINAL_PANEL, + onExpand, + onCollapse, + }); const handleInteraction = useCallback(async (_itemId: string, _userInput: string) => { // Terminal tabs do not use ContentCanvas interaction callbacks. }, []); @@ -40,10 +50,8 @@ const BottomTerminalPane: React.FC = ({ onBeforeClose={handleBeforeClose} disablePopOut createTabEventName={TAB_EVENTS.BOTTOM_TERMINAL_CREATE_TAB} - expandPanelEventName={TAB_EVENTS.EXPAND_BOTTOM_TERMINAL_PANEL} - isPanelCollapsed={isCollapsed} - onExpandPanel={onExpand} - onCollapsePanel={onCollapse} + onReveal={expandPanel} + onCollapsePanel={collapsePanel} terminalResizeSuspended={terminalResizeSuspended} />
    diff --git a/src/web-ui/src/app/scenes/session/SessionScene.tsx b/src/web-ui/src/app/scenes/session/SessionScene.tsx index 50c2155cdf..93a93dfa54 100644 --- a/src/web-ui/src/app/scenes/session/SessionScene.tsx +++ b/src/web-ui/src/app/scenes/session/SessionScene.tsx @@ -15,6 +15,11 @@ import { useApp } from '../../hooks/useApp'; import ChatPane from './ChatPane'; import AuxPane, { type AuxPaneRef } from './AuxPane'; import BottomTerminalPane from './BottomTerminalPane'; +import { + collapseSessionBottomTerminalPane, + expandSessionAuxPane, + expandSessionBottomTerminalPane, +} from './sessionPanelLayout'; import { getCachedTerminalPanelPosition, onTerminalPanelPositionChange, @@ -57,7 +62,6 @@ const SessionScene: React.FC = ({ updateRightPanelWidth, toggleRightPanel, updateBottomTerminalPanelHeight, - toggleBottomTerminalPanel, } = useApp(); const auxPaneRef = useRef(null); @@ -80,8 +84,6 @@ const SessionScene: React.FC = ({ const auxPaneElementRef = useRef(null); const bottomTerminalPaneElementRef = useRef(null); const animationFrameRef = useRef(null); - const bottomExpandPendingRef = useRef(false); - const bottomCollapsePendingRef = useRef(false); const rightPanelTransitionTimerRef = useRef(null); const bottomPanelTransitionTimerRef = useRef(null); const previousRightTransitionKeyRef = useRef(null); @@ -149,9 +151,9 @@ const SessionScene: React.FC = ({ // Keep right panel visible when chat is hidden useEffect(() => { if (state.layout.chatCollapsed && state.layout.rightPanelCollapsed) { - toggleRightPanel(); + expandSessionAuxPane(); } - }, [state.layout.chatCollapsed, state.layout.rightPanelCollapsed, toggleRightPanel]); + }, [state.layout.chatCollapsed, state.layout.rightPanelCollapsed]); const calculateValidRightWidth = useCallback((newWidth: number): number => { if (!containerRef.current) return newWidth; @@ -313,31 +315,8 @@ const SessionScene: React.FC = ({ STORAGE_KEYS.BOTTOM_TERMINAL_PANEL_LAST_HEIGHT, BOTTOM_TERMINAL_PANEL_CONFIG.COMFORTABLE_DEFAULT, ); - updateBottomTerminalPanelHeight(calculateValidBottomHeight(saved)); - - if (state.layout.bottomTerminalPanelCollapsed && !bottomExpandPendingRef.current) { - bottomExpandPendingRef.current = true; - requestAnimationFrame(() => { - toggleBottomTerminalPanel(); - bottomExpandPendingRef.current = false; - }); - } - }, [ - calculateValidBottomHeight, - state.layout.bottomTerminalPanelCollapsed, - toggleBottomTerminalPanel, - updateBottomTerminalPanelHeight, - ]); - - const collapseBottomTerminalPanel = useCallback(() => { - if (!state.layout.bottomTerminalPanelCollapsed && !bottomCollapsePendingRef.current) { - bottomCollapsePendingRef.current = true; - requestAnimationFrame(() => { - toggleBottomTerminalPanel(); - bottomCollapsePendingRef.current = false; - }); - } - }, [state.layout.bottomTerminalPanelCollapsed, toggleBottomTerminalPanel]); + expandSessionBottomTerminalPane(calculateValidBottomHeight(saved)); + }, [calculateValidBottomHeight]); // Responsive resize — also validate on mount to clamp widths restored from // localStorage that may exceed the current (non-maximized) window size. @@ -656,9 +635,8 @@ const SessionScene: React.FC = ({
    diff --git a/src/web-ui/src/app/scenes/session/sessionPanelLayout.ts b/src/web-ui/src/app/scenes/session/sessionPanelLayout.ts new file mode 100644 index 0000000000..80b7f53564 --- /dev/null +++ b/src/web-ui/src/app/scenes/session/sessionPanelLayout.ts @@ -0,0 +1,35 @@ +import { appManager } from '../../services/AppManager'; +import { loadPanelWidth, RIGHT_PANEL_CONFIG, STORAGE_KEYS } from '../../layout/panelConfig'; + +/** Explicit, synchronous layout writes remain safe when open requests repeat. */ +export function expandSessionAuxPane(): void { + if (!appManager.getState().layout.rightPanelCollapsed) return; + + window.dispatchEvent(new CustomEvent('expand-right-panel-immediate', { + detail: { noAnimation: true }, + })); + appManager.updateLayout({ + rightPanelWidth: loadPanelWidth( + STORAGE_KEYS.RIGHT_PANEL_LAST_WIDTH, + RIGHT_PANEL_CONFIG.COMFORTABLE_DEFAULT, + ), + rightPanelCollapsed: false, + }); +} + +export function collapseSessionAuxPane(): void { + // In editor mode this pane is the main content surface. + if (appManager.getState().layout.chatCollapsed) return; + appManager.updateLayout({ rightPanelCollapsed: true }); +} + +export function expandSessionBottomTerminalPane(height: number): void { + appManager.updateLayout({ + bottomTerminalPanelHeight: height, + bottomTerminalPanelCollapsed: false, + }); +} + +export function collapseSessionBottomTerminalPane(): void { + appManager.updateLayout({ bottomTerminalPanelCollapsed: true }); +} diff --git a/src/web-ui/src/app/scenes/skills/SkillsScene.presentation.test.ts b/src/web-ui/src/app/scenes/skills/SkillsScene.presentation.test.ts index 78c028752c..002a45853d 100644 --- a/src/web-ui/src/app/scenes/skills/SkillsScene.presentation.test.ts +++ b/src/web-ui/src/app/scenes/skills/SkillsScene.presentation.test.ts @@ -13,57 +13,56 @@ describe('Skills scene presentation', () => { it('keeps the installed collection in one continuous scroll region', () => { const source = readSibling('./SkillsScene.tsx'); - expect(source).toContain('{installedFiltered.map((skill, index) => ('); - expect(source).toContain(' ('); + expect(source).toMatch(/ { const stylesheet = readSibling('./SkillsScene.scss'); + const listStylesheet = readSibling('./components/_SkillsList.scss'); const shellStart = stylesheet.indexOf('.skills-main__list-shell {'); const shellEnd = stylesheet.indexOf('.skills-main__list-header,', shellStart); const scrollStart = stylesheet.indexOf('.skills-main__grid,'); const scrollEnd = stylesheet.indexOf('.skills-main__grid {', scrollStart); expect(stylesheet.slice(shellStart, shellEnd)).toContain('overflow: hidden;'); - expect(stylesheet.slice(shellStart, shellEnd)).toContain('padding: 12px 16px 16px;'); + expect(stylesheet.slice(shellStart, shellEnd)).toContain('padding: var(--openbitfun-space-2) var(--openbitfun-space-6) var(--openbitfun-space-6);'); expect(stylesheet.slice(scrollStart, scrollEnd)).toContain('flex: 0 1 auto;'); - expect(stylesheet).toContain('min-height: 64px;'); + expect(stylesheet).toContain('@include skills-list.row;'); + expect(listStylesheet).toContain('min-block-size: 88px;'); }); - it('uses compact dimensions that fit the default desktop scene', () => { + it('uses the ecosystem navigation width and shared content typography', () => { const stylesheet = readSibling('./SkillsScene.scss'); - expect(stylesheet).toContain('$skills-sidebar-width: 184px;'); + expect(stylesheet).toContain('$skills-sidebar-width: 264px;'); expect(stylesheet).toContain('min-height: 32px;'); - expect(stylesheet).toContain('font-size: var(--openbitfun-type-flow-section-title-font-size);'); + expect(stylesheet).toContain('font-size: var(--openbitfun-type-heading-dialog-font-size);'); }); it('lets the skills page inherit the surrounding scene surface', () => { const stylesheet = readSibling('./SkillsScene.scss'); - const listSurfaceStart = stylesheet.indexOf('.skills-main__grid,'); - const listSurfaceEnd = stylesheet.indexOf('\n\n.skills-main__grid {', listSurfaceStart); - const discoverHeroStart = stylesheet.indexOf('.skills-discover__hero {'); - const discoverHeroEnd = stylesheet.indexOf('\n}', discoverHeroStart); + const listStylesheet = readSibling('./components/_SkillsList.scss'); + const listSurfaceStart = stylesheet.indexOf('.skills-main__table {'); + const listSurfaceEnd = stylesheet.indexOf('\n}', listSurfaceStart); + const surfaceStart = listStylesheet.indexOf('@mixin surface {'); + const surfaceEnd = listStylesheet.indexOf('\n}', surfaceStart); + const headerStart = stylesheet.indexOf('.skills-content-header {'); + const headerEnd = stylesheet.indexOf('\n}', headerStart); expect(stylesheet).not.toContain('background: var(--openbitfun-color-surface-canvas);'); - expect(stylesheet.slice(listSurfaceStart, listSurfaceEnd)).toContain('background: var(--openbitfun-color-surface-tertiary);'); - expect(stylesheet.slice(discoverHeroStart, discoverHeroEnd)).not.toContain('background:'); + expect(stylesheet.slice(listSurfaceStart, listSurfaceEnd)).toContain('@include skills-list.surface;'); + expect(listStylesheet.slice(surfaceStart, surfaceEnd)).toContain('background: transparent;'); + expect(stylesheet.slice(headerStart, headerEnd)).not.toContain('background:'); }); - it('keeps the suite scrollbar on the scene edge without moving its content', () => { - const stylesheet = readSibling('./SkillsScene.scss'); - const suiteStart = stylesheet.indexOf('.skills-suite {'); - const suiteEnd = stylesheet.indexOf('\n}', suiteStart); - const suite = stylesheet.slice(suiteStart, suiteEnd); - const sectionsStart = stylesheet.indexOf('.skills-suite__sections {'); - const sectionsEnd = stylesheet.indexOf('\n}', sectionsStart); - const sections = stylesheet.slice(sectionsStart, sectionsEnd); - - expect(suite).toContain('padding-inline: 24px 0;'); - expect(sections).toContain( - 'padding-inline-end: calc(var(--skills-suite-inline-end-inset) + 4px);', + it('keeps the group scrollbar on the scene edge without moving its content', () => { + const stylesheet = readSibling('./components/SkillGroupsView.scss'); + expect(stylesheet).toContain('--skill-groups-inline-inset: var(--openbitfun-space-6);'); + expect(stylesheet).toContain( + 'padding-inline: var(--skill-groups-inline-inset) calc(var(--skill-groups-inline-inset) + var(--openbitfun-space-1));', ); }); @@ -74,7 +73,7 @@ describe('Skills scene presentation', () => { expect(source).toContain('className="skills-card__actions"'); expect(source).toContain('data-openbitfun-part="installedCardDetails"'); expect(source).toContain('data-openbitfun-part="installedCardDelete"'); - expect(stylesheet).toContain('64px 64px;'); + expect(stylesheet).toContain('minmax(120px, 0.85fr) 64px;'); expect(stylesheet).toContain('.skills-card__actions {'); }); }); diff --git a/src/web-ui/src/app/scenes/skills/SkillsScene.scss b/src/web-ui/src/app/scenes/skills/SkillsScene.scss index a45618f52a..0b0c25f416 100644 --- a/src/web-ui/src/app/scenes/skills/SkillsScene.scss +++ b/src/web-ui/src/app/scenes/skills/SkillsScene.scss @@ -1,15 +1,17 @@ @use '../../styles/surface-stagger'; +@use './components/SkillsList' as skills-list; -$skills-sidebar-width: 184px; -$skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, 0.3fr) 64px 64px; +$skills-sidebar-width: 264px; +$skills-installed-columns: minmax(0, 1.6fr) minmax(0, 0.9fr) minmax(120px, 0.85fr) 64px; // ══════════════════════════════════════════════════════════════════════════════ // Root wrapper // ══════════════════════════════════════════════════════════════════════════════ .openbitfun-skills-scene { - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr); width: 100%; height: 100%; min-width: 0; @@ -19,45 +21,18 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, color: var(--openbitfun-color-content-primary); } -.openbitfun-skills-scene > .gallery-page-header { - position: static; - flex: 0 0 auto; - width: 100%; - padding: var(--openbitfun-space-5) var(--openbitfun-space-6) var(--openbitfun-space-4); -} - -// ══════════════════════════════════════════════════════════════════════════════ -// Top-left tabs bar -// ══════════════════════════════════════════════════════════════════════════════ - -.skills-tabs-bar { - flex-shrink: 0; - display: flex; - align-items: center; - padding: 0 var(--openbitfun-space-6) var(--openbitfun-space-3); - border-bottom: 1px solid var(--openbitfun-color-border-subtle); -} - -.skills-tabs-bar__tabs { - display: flex; - align-items: center; - gap: var(--openbitfun-space-2); -} - -.skills-tabs-bar__divider { - display: none; -} - // ══════════════════════════════════════════════════════════════════════════════ // Page container // ══════════════════════════════════════════════════════════════════════════════ .skills-page { - flex: 1; + grid-area: 1 / 2 / 3 / 3; + min-width: 0; min-height: 0; overflow: hidden; display: flex; flex-direction: column; + container: skills-content / inline-size; } // Installed collection @@ -70,33 +45,30 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, } .skills-sidebar { - width: $skills-sidebar-width; - flex-shrink: 0; - display: flex; - flex-direction: column; - min-height: 0; - border-inline-end: 1px solid var(--openbitfun-color-border-subtle); + grid-area: 1 / 1 / 3 / 2; + inline-size: $skills-sidebar-width; + min-inline-size: 0; + border-inline-end: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); + background: transparent; } .skills-sidebar__header { - flex-shrink: 0; - padding: var(--openbitfun-space-4) var(--openbitfun-space-4) var(--openbitfun-space-2); + display: grid; + gap: var(--openbitfun-space-3); + padding: var(--openbitfun-space-4); } .skills-sidebar__title { margin: 0; - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-meta-font-size); + color: var(--openbitfun-color-content-primary); + font-size: var(--openbitfun-type-label-md-font-size); font-weight: var(--openbitfun-type-label-selected-font-weight); - line-height: var(--openbitfun-type-meta-line-height); + line-height: var(--openbitfun-type-label-md-line-height); } -.skills-sidebar__nav { - flex: 1; - display: flex; - flex-direction: column; - gap: var(--openbitfun-space-1); - padding: 0 var(--openbitfun-space-2) var(--openbitfun-space-4); +.skills-sidebar__compact-picker { + display: none; + min-inline-size: 0; } .skills-sidebar__item { @@ -109,20 +81,7 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, color: var(--openbitfun-color-content-muted); font-size: var(--openbitfun-type-meta-font-size); font-variant-numeric: tabular-nums; - text-align: end; -} - -.skills-sidebar__footer { - flex-shrink: 0; - padding: var(--openbitfun-space-4); - border-top: 1px solid var(--openbitfun-color-border-subtle); -} - -.skills-sidebar__hint { - margin: 0; - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-meta-font-size); - line-height: var(--openbitfun-type-flow-support-line-height); + text-align: center; } .skills-main { @@ -134,272 +93,73 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, overflow: hidden; } -.skills-main__toolbar { - flex-shrink: 0; +.skills-content-header { + flex: 0 0 auto; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; - gap: var(--openbitfun-space-3); - padding: var(--openbitfun-space-4) var(--openbitfun-space-4) var(--openbitfun-space-1); -} - -.skills-main__toolbar-search { - flex: 1 1 220px; - min-width: 0; - max-width: 480px; -} - -.skills-main__filter { - flex-shrink: 0; - margin-inline-start: auto; -} - -// ── Suite Page ─────────────────────────────────────────────────────────────── - -.skills-suite { - --skills-suite-inline-end-inset: 24px; - - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - gap: 14px; - padding-block: 20px 18px; - padding-inline: 24px 0; - overflow: hidden; -} - -.skills-suite__hero, -.skills-suite__mode-toolbar, -.skills-suite__loading, -.skills-suite > .skills-main__empty { - margin-inline-end: var(--skills-suite-inline-end-inset); -} - -.skills-suite__hero { - display: flex; - flex-wrap: wrap; - align-items: flex-start; - justify-content: space-between; gap: var(--openbitfun-space-4); + padding: var(--openbitfun-space-6); + border-block-end: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); } -.skills-suite__hero-copy { +.skills-content-header__identity { display: flex; - flex-direction: column; - gap: 6px; - min-width: 0; -} - -.skills-suite__hero-actions { - display: inline-flex; + flex: 1 1 240px; align-items: center; - gap: 8px; - flex-shrink: 0; - flex-wrap: wrap; + min-inline-size: 0; } -.skills-suite__eyebrow { - font-size: var(--openbitfun-type-meta-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - letter-spacing: var(--openbitfun-type-modifier-tracking-caps-letter-spacing); - text-transform: uppercase; - color: var(--openbitfun-color-accent-default); +.skills-content-header__copy { + flex: 1; + min-inline-size: 0; } -.skills-suite__title { +.skills-content-header__title { + min-inline-size: 0; margin: 0; + font-family: var(--openbitfun-type-heading-dialog-font-family); font-size: var(--openbitfun-type-heading-dialog-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - line-height: var(--openbitfun-type-label-md-line-height); - color: var(--openbitfun-color-content-primary); + font-weight: var(--openbitfun-type-heading-dialog-font-weight); + line-height: var(--openbitfun-type-heading-dialog-line-height); + letter-spacing: var(--openbitfun-type-heading-dialog-letter-spacing); } -.skills-suite__subtitle { - margin: 0; - max-width: 720px; +.skills-content-header__description { + margin: var(--openbitfun-space-1) 0 0; color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-label-md-font-size); - line-height: var(--openbitfun-type-support-line-height); -} - -.skills-suite__mode-toolbar { - display: flex; - align-items: center; - gap: var(--openbitfun-space-3); - padding-bottom: var(--openbitfun-space-3); - border-bottom: 1px solid var(--openbitfun-color-border-subtle); + font-size: var(--openbitfun-type-body-sm-font-size); + line-height: var(--openbitfun-type-body-sm-line-height); + overflow-wrap: anywhere; } -.skills-suite__modes { - flex: 1; - min-width: 0; - overflow-x: auto; +.skills-content-header__action { + flex: none; } -.skills-suite__mode-reset { +.skills-main__toolbar { flex-shrink: 0; -} - -.skills-suite__mode-hint { - display: inline-flex; - align-items: center; - gap: 7px; - font-size: var(--openbitfun-type-meta-font-size); - color: var(--openbitfun-color-content-muted); -} - -.skills-suite__loading { - flex: 1; - min-height: 0; - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - color: var(--openbitfun-color-content-muted); -} - -.skills-suite__loading-icon { - animation: skills-suite-spin 0.9s linear infinite; -} - -.skills-suite__sections { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - gap: 16px; - padding-inline-end: calc(var(--skills-suite-inline-end-inset) + 4px); -} - -.skills-suite__section { - display: flex; - flex-direction: column; - gap: 8px; -} - -.skills-suite__section-label { - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-meta-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - letter-spacing: var(--openbitfun-type-modifier-tracking-wider-letter-spacing); - text-transform: uppercase; -} - -.skills-suite__grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); - gap: 14px; - align-content: start; -} - -.skills-suite__group-card { - display: flex; - flex-direction: column; - min-width: 0; - gap: var(--openbitfun-space-4); - padding: var(--openbitfun-space-4); - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-layout-field-group-radius); - background: var(--openbitfun-color-surface-tertiary); -} - -.skills-suite__group-head { display: flex; flex-wrap: wrap; - align-items: flex-start; - justify-content: space-between; - gap: 12px; -} - -.skills-suite__group-actions { - display: inline-flex; - align-items: center; - gap: 8px; - flex-shrink: 0; -} - -.skills-suite__group-title-wrap { - display: flex; - flex-direction: column; - gap: 4px; - min-width: 0; -} - -.skills-suite__group-title-row { - display: flex; align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.skills-suite__group-title { - display: flex; - min-width: 0; - max-width: 100%; - font-size: var(--openbitfun-type-label-md-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - color: var(--openbitfun-color-content-primary); -} - -.skills-suite__group-count { - font-size: var(--openbitfun-type-meta-font-size); - color: var(--openbitfun-color-content-muted); -} - -.skills-suite__skills { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); - gap: var(--openbitfun-space-2); -} - -.skills-suite__skill-chip { - display: flex; - min-width: 0; - padding: var(--openbitfun-space-2) var(--openbitfun-space-3); - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-radius-base); - background: var(--openbitfun-color-surface-subtle); - - &.is-enabled { - background: var(--openbitfun-color-selection-surface); - } - - &.is-shadowed { - border-color: var(--openbitfun-color-status-warning-border); - } - - &.is-dirty { - border-color: var(--openbitfun-color-accent-default); - } -} - -.skills-suite__skill-control { - width: 100%; - min-width: 0; + justify-content: space-between; + gap: var(--openbitfun-space-3); + padding: var(--openbitfun-space-5) var(--openbitfun-space-6) var(--openbitfun-space-3); } -.skills-suite__skill-chip-name { - display: flex; +.skills-main__toolbar-search { + flex: 1 1 220px; min-width: 0; - color: var(--openbitfun-color-content-primary); -} - -.skills-suite__skill-chip-status { - display: block; - color: var(--openbitfun-color-status-warning-content); - font-size: var(--openbitfun-type-meta-font-size); - line-height: var(--openbitfun-type-meta-line-height); - overflow-wrap: anywhere; + max-width: 480px; } -@keyframes skills-suite-spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } +.skills-main__filter { + flex-shrink: 0; + margin-inline-start: auto; } -// ── Compact installed list ── +// ── Installed skills table ── .skills-main__list-shell { flex: 1; @@ -407,69 +167,56 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, display: flex; flex-direction: column; overflow: hidden; - padding: 12px 16px 16px; + padding: var(--openbitfun-space-2) var(--openbitfun-space-6) var(--openbitfun-space-6); } .skills-main__list-header, -.skills-card { +.skills-card, +.skills-card-skeleton { display: grid; grid-template-columns: $skills-installed-columns; - column-gap: 12px; + grid-template-areas: 'identity source status actions'; + column-gap: var(--openbitfun-space-4); + min-inline-size: 0; } .skills-main__list-header { - flex-shrink: 0; - align-items: center; - min-height: 28px; - padding: 0 12px 8px; - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-meta-font-size); - line-height: var(--openbitfun-type-label-md-line-height); + @include skills-list.header; + // Match the body columns while painting the header through the native gutter. + overflow: hidden; + scrollbar-gutter: stable; } .skills-main__list-heading { display: flex; align-items: center; min-width: 0; - gap: 7px; - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-label-sm-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); + gap: var(--openbitfun-space-2); } .skills-main__list-count { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 18px; - height: 18px; - padding: 0 5px; - border-radius: var(--openbitfun-radius-pill); - background: var(--openbitfun-color-action-neutral-surface-hover); - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-micro-font-size); - font-weight: var(--openbitfun-type-label-sm-font-weight); + @include skills-list.count; } .skills-main__column-label { white-space: nowrap; - &--status { - text-align: center; - } - &--actions { - text-align: center; + text-align: end; } } +.skills-main__table { + @include skills-list.surface; + flex: 0 1 auto; + min-height: 0; +} + .skills-main__grid, .skills-main__loading { flex: 0 1 auto; min-height: 0; - border: 1px solid var(--openbitfun-color-border-subtle); - border-radius: var(--openbitfun-layout-field-group-radius); - background: var(--openbitfun-color-surface-tertiary); + scrollbar-gutter: stable; } .skills-main__grid { @@ -478,65 +225,37 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, } .skills-card { - flex-shrink: 0; - align-items: center; - min-height: 64px; - padding: var(--openbitfun-space-3); - border-bottom: 1px solid var(--openbitfun-color-border-subtle); - background: transparent; - cursor: pointer; - transition: background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - &:last-child { - border-bottom: none; - } - - &:hover { - background: var(--openbitfun-color-action-neutral-surface-hover); - } - - &:focus-visible { - position: relative; - z-index: 1; - outline: 2px solid var(--openbitfun-color-accent-default); - outline-offset: -2px; - } - - &.is-shadowed { - background: var(--openbitfun-color-surface-subtle); + @include skills-list.row; + &.is-shadowed, + &.is-globally-disabled { .skills-card__name { - text-decoration: line-through; - opacity: 0.75; + color: var(--openbitfun-color-content-secondary); } } +} - &.is-globally-disabled { - .skills-card__icon, - .skills-card__info, - .skills-card__meta, - .skills-card__level { - opacity: 0.68; - } - } +.skills-card__open { + @include skills-list.open-target; } .skills-card__top { + grid-area: identity; display: flex; align-items: flex-start; - gap: 10px; + gap: var(--openbitfun-space-3); min-width: 0; } .skills-card__icon { flex-shrink: 0; - width: 32px; - height: 32px; + inline-size: var(--openbitfun-control-height-sm); + block-size: var(--openbitfun-control-height-sm); display: flex; align-items: center; justify-content: center; border-radius: var(--openbitfun-radius-base); - background: var(--openbitfun-color-surface-subtle); + background: var(--openbitfun-color-surface-tertiary); color: var(--openbitfun-color-content-secondary); } @@ -545,18 +264,17 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, min-width: 0; display: flex; flex-direction: column; - gap: 2px; + gap: var(--openbitfun-space-1); } -.skills-card__status-badges { +.skills-card__title-row { display: flex; - flex-wrap: wrap; - gap: var(--openbitfun-space-1); + align-items: center; + gap: var(--openbitfun-space-2); min-width: 0; - margin-top: var(--openbitfun-space-1); - &:empty { - display: none; + > [data-openbitfun-component='status-pill'] { + flex: none; } } @@ -564,15 +282,15 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, display: flex; min-width: 0; color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-label-md-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - line-height: var(--openbitfun-type-label-md-line-height); + font-size: var(--openbitfun-type-label-sm-font-size); + font-weight: var(--openbitfun-type-label-sm-font-weight); + line-height: var(--openbitfun-type-label-sm-line-height); } .skills-card__desc { - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-body-sm-font-size); - line-height: var(--openbitfun-type-body-sm-line-height); + color: var(--openbitfun-color-content-muted); + font-size: var(--openbitfun-type-meta-font-size); + line-height: var(--openbitfun-type-meta-line-height); display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; @@ -591,23 +309,27 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, } .skills-card__meta { - overflow: hidden; - - .skills-card__source { - display: flex; - min-width: 0; - max-width: 100%; - } + grid-area: source; + flex-direction: column; + align-items: stretch; + gap: var(--openbitfun-space-1); +} - .skills-card__source-pill { - max-width: 100%; - } +.skills-card__source { + display: flex; + min-width: 0; + max-width: 100%; + color: var(--openbitfun-color-content-secondary); + font-size: var(--openbitfun-type-label-sm-font-size); + font-weight: var(--openbitfun-type-label-sm-font-weight); + line-height: var(--openbitfun-type-label-sm-line-height); } .skills-card__level { gap: var(--openbitfun-space-1); - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-label-sm-font-size); + color: var(--openbitfun-color-content-muted); + font-size: var(--openbitfun-type-meta-font-size); + line-height: var(--openbitfun-type-meta-line-height); > [data-openbitfun-component='icon'] { flex-shrink: 0; @@ -615,7 +337,33 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, } .skills-card__global-toggle { - justify-content: center; + grid-area: status; + flex-direction: column; + align-items: flex-start; + gap: var(--openbitfun-space-2); + + > [data-openbitfun-component='status-pill'] { + max-inline-size: 100%; + } +} + +.skills-card__global-toggle, +.skills-card__actions { + position: relative; + z-index: 2; +} + +.skills-card__availability { + display: flex; + align-items: center; + min-inline-size: 0; + gap: var(--openbitfun-space-2); +} + +.skills-card__availability-label { + color: var(--openbitfun-color-content-secondary); + font-size: var(--openbitfun-type-meta-font-size); + line-height: var(--openbitfun-type-meta-line-height); } .skills-card__status-unavailable { @@ -624,8 +372,9 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, } .skills-card__actions { + grid-area: actions; justify-content: flex-end; - gap: 4px; + gap: var(--openbitfun-space-2); } // ══════════════════════════════════════════════════════════════════════════════ @@ -639,14 +388,43 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, .skills-card-skeleton { flex-shrink: 0; - height: 64px; - border-bottom: 1px solid var(--openbitfun-color-border-subtle); - background: var(--openbitfun-color-surface-subtle); + align-items: center; + min-block-size: 88px; + padding: var(--openbitfun-space-4); + border-block-end: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); animation: skills-card-in 0.2s var(--openbitfun-motion-easing-decelerate) both; @include surface-stagger.animation-delay(32ms); &:last-child { - border-bottom: none; + border-block-end: 0; + } +} + +.skills-card-skeleton__icon, +.skills-card-skeleton__line { + display: block; + border-radius: var(--openbitfun-radius-xs); + background: var(--openbitfun-color-action-neutral-surface-hover); +} + +.skills-card-skeleton__icon { + flex: none; + inline-size: var(--openbitfun-control-height-sm); + block-size: var(--openbitfun-control-height-sm); + border-radius: var(--openbitfun-radius-base); +} + +.skills-card-skeleton__line { + inline-size: 100%; + block-size: var(--openbitfun-space-3); + + &--title { + inline-size: 70%; + block-size: var(--openbitfun-space-4); + } + + &--short { + inline-size: 60%; } } @@ -658,9 +436,11 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, flex-shrink: 0; display: flex; align-items: center; - justify-content: center; - gap: 10px; - padding: 8px 0; + justify-content: flex-end; + flex-wrap: wrap; + gap: var(--openbitfun-space-3); + padding: var(--openbitfun-space-3) var(--openbitfun-space-6); + border-block-start: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); } .skills-discover__page-btn { @@ -670,16 +450,17 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, .skills-discover__page-info { min-width: 60px; text-align: center; - font-size: var(--openbitfun-type-label-md-font-size); - color: var(--openbitfun-color-content-secondary); + font-size: var(--openbitfun-type-meta-font-size); + line-height: var(--openbitfun-type-meta-line-height); + font-variant-numeric: tabular-nums; + color: var(--openbitfun-color-content-muted); } // ══════════════════════════════════════════════════════════════════════════════ // Empty / Error // ══════════════════════════════════════════════════════════════════════════════ -.skills-main__empty, -.skills-discover__empty { +.skills-main__empty { display: flex; flex-direction: column; align-items: center; @@ -699,8 +480,7 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, } } -.skills-installed__empty, -.skills-discover__empty { +.skills-installed__empty { align-items: center; justify-content: center; gap: 12px; @@ -717,76 +497,51 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, display: flex; flex-direction: column; height: 100%; + min-height: 0; overflow: hidden; } -.skills-discover__hero { +.skills-discover__toolbar { flex-shrink: 0; display: flex; - padding: var(--openbitfun-space-5) var(--openbitfun-space-6) var(--openbitfun-space-4); -} - -.skills-discover__hero-content { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(240px, 0.8fr); align-items: center; - column-gap: var(--openbitfun-space-6); - row-gap: var(--openbitfun-space-1); - width: 100%; - min-width: 0; -} - -.skills-discover__title { - margin: 0; - font-size: var(--openbitfun-type-flow-section-title-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - line-height: var(--openbitfun-type-label-md-line-height); -} - -.skills-discover__subtitle { - grid-column: 1; - margin: 0; - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-body-sm-font-size); - line-height: var(--openbitfun-type-body-sm-line-height); -} - -.skills-discover__search-wrapper { - grid-column: 2; - grid-row: 1 / 3; + flex-wrap: wrap; + gap: var(--openbitfun-space-3); min-width: 0; + padding: var(--openbitfun-space-5) var(--openbitfun-space-6) var(--openbitfun-space-4); } .skills-discover__search { - width: 100%; + flex: 1 1 220px; + min-width: 0; + max-width: 480px; } .skills-discover__content { flex: 1; min-height: 0; - padding: var(--openbitfun-space-1) var(--openbitfun-space-6) var(--openbitfun-space-6); + padding: 0 var(--openbitfun-space-6) var(--openbitfun-space-6); } .skills-discover__results-info { - margin-bottom: 10px; - font-size: var(--openbitfun-type-label-sm-font-size); + display: flex; + flex: 1 1 220px; + min-inline-size: 0; + font-size: var(--openbitfun-type-meta-font-size); + line-height: var(--openbitfun-type-meta-line-height); color: var(--openbitfun-color-content-muted); } .skills-discover__grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(min(240px, 100%), 1fr)); + grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr)); gap: var(--openbitfun-space-4); align-items: stretch; } -.skills-discover__skeleton-card { - min-height: 196px; - border-radius: var(--openbitfun-radius-lg); - background: var(--openbitfun-color-surface-subtle); - border: 1px solid var(--openbitfun-color-border-subtle); - animation: skills-card-in 0.18s var(--openbitfun-motion-easing-decelerate) both; - @include surface-stagger.animation-delay(40ms); +.skills-discover__state { + min-block-size: 200px; + overflow-wrap: anywhere; } // ══════════════════════════════════════════════════════════════════════════════ @@ -797,7 +552,8 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, &__market-meta { display: inline-flex; align-items: center; - gap: 4px; + gap: var(--openbitfun-space-1); + min-inline-size: 0; font-size: var(--openbitfun-type-label-sm-font-size); color: var(--openbitfun-color-content-muted); } @@ -922,120 +678,94 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, // Responsive // ══════════════════════════════════════════════════════════════════════════════ -@container skills-scene (max-width: 1040px) { - .skills-main__list-header, - .skills-card { - grid-template-columns: minmax(0, 1fr) 64px 64px; +@container skills-content (max-width: 680px) { + .skills-content-header, + .skills-main__toolbar, + .skills-main__list-shell, + .skills-discover__toolbar, + .skills-discover__pagination, + .skills-discover__content { + padding-inline: var(--openbitfun-space-4); } - .skills-main__column-label--source, - .skills-main__column-label--level { - display: none; + .skills-main__toolbar-search, + .skills-discover__search { + max-width: none; } - .skills-card { - grid-template-areas: - 'identity status actions' - 'source level level'; - row-gap: var(--openbitfun-space-2); + .skills-main__list-header { + display: flex; } - .skills-card__top { - grid-area: identity; + .skills-main__column-label { + position: absolute; + inline-size: 1px; + block-size: 1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); } - .skills-card__meta { - grid-area: source; - padding-inline-start: 42px; + .skills-card, + .skills-card-skeleton { + grid-template-columns: minmax(0, 1fr) minmax(120px, auto) 64px; + grid-template-areas: + 'identity identity actions' + 'source status status'; + row-gap: var(--openbitfun-space-3); } - .skills-card__level { - grid-area: level; - justify-content: flex-end; + .skills-card__meta { + padding-inline-start: calc(var(--openbitfun-control-height-sm) + var(--openbitfun-space-3)); } .skills-card__global-toggle { - grid-area: status; - } - - .skills-card__actions { - grid-area: actions; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; } } @container skills-scene (max-width: 760px) { - .openbitfun-skills-scene > .gallery-page-header { - padding: var(--openbitfun-space-4); - } - - .skills-tabs-bar { - padding: 0 var(--openbitfun-space-4) var(--openbitfun-space-3); - } - - .skills-installed { - flex-direction: column; - } - .skills-sidebar { - width: 100%; - border-inline-end: none; - border-bottom: 1px solid var(--openbitfun-color-border-subtle); - } - - .skills-sidebar__header, - .skills-sidebar__footer { - display: none; - } - - .skills-sidebar__nav { - flex-direction: row; - flex-wrap: wrap; - padding: var(--openbitfun-space-2); - } - - .skills-sidebar__item { - flex: 1 1 100px; - } - - .skills-main__toolbar-search { - max-width: none; - } - - .skills-suite { - --skills-suite-inline-end-inset: 16px; - - padding-block: 16px; - padding-inline: 16px 0; - } - - .skills-suite__hero { - flex-direction: column; + grid-area: 1 / 1 / 2 / 3; + inline-size: 100%; + block-size: auto; + border-inline-end: 0; + border-block-end: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); + + > [data-openbitfun-part='body'] { + display: none; + } } - .skills-suite__grid { - grid-template-columns: 1fr; + .skills-sidebar__header { + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: var(--openbitfun-space-4); + padding: var(--openbitfun-space-3) var(--openbitfun-space-4); } - .skills-discover__hero { - padding: var(--openbitfun-space-4); + .skills-sidebar__compact-picker { + display: block; } - .skills-discover__hero-content { - grid-template-columns: minmax(0, 1fr); + .skills-page { + grid-area: 2 / 1 / 3 / 3; } +} - .skills-discover__search-wrapper { - grid-column: 1; - grid-row: auto; - margin-top: var(--openbitfun-space-3); +@container skills-content (max-width: 480px) { + .skills-content-header { + padding-block: var(--openbitfun-space-4); } - .skills-discover__content { - padding: 0 var(--openbitfun-space-4) var(--openbitfun-space-4); + .skills-discover__pagination { + justify-content: center; + gap: var(--openbitfun-space-2); } -} -@container skills-scene (max-width: 480px) { .skills-main__toolbar-search { flex-basis: 100%; } @@ -1044,37 +774,31 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, margin-inline-start: 0; } - .skills-main__list-header { - display: flex; - } - - .skills-main__column-label { - display: none; - } - - .skills-card { + .skills-card, + .skills-card-skeleton { grid-template-columns: minmax(0, 1fr) 64px; grid-template-areas: - 'identity actions' - 'source status' - 'level status'; + 'identity identity' + 'source source' + 'status actions'; } - .skills-card__level { - padding-inline-start: 42px; - justify-content: flex-start; + .skills-card__meta { + flex-direction: row; + flex-wrap: wrap; + align-items: center; + gap: var(--openbitfun-space-2) var(--openbitfun-space-3); } .skills-card__global-toggle { - justify-content: flex-end; + padding-inline-start: calc(var(--openbitfun-control-height-sm) + var(--openbitfun-space-3)); + justify-content: flex-start; } } @media (prefers-reduced-motion: reduce) { .skills-card, - .skills-card-skeleton, - .skills-discover__skeleton-card, - .skills-suite__loading-icon { + .skills-card-skeleton { animation: none; transition: none; } @@ -1083,13 +807,3 @@ $skills-installed-columns: minmax(200px, 1fr) minmax(90px, 0.24fr) minmax(110px, .skills-main > .skills-main__empty { margin: var(--openbitfun-space-4) var(--openbitfun-space-6); } - -.skills-main__diagnostics { - padding: 8px 12px; - max-height: 160px; - overflow: auto; - overflow-wrap: anywhere; - font-size: var(--openbitfun-type-flow-support-font-size); - - summary { cursor: pointer; } -} diff --git a/src/web-ui/src/app/scenes/skills/SkillsScene.tsx b/src/web-ui/src/app/scenes/skills/SkillsScene.tsx index 66e4e2ffeb..6471c0e6d0 100644 --- a/src/web-ui/src/app/scenes/skills/SkillsScene.tsx +++ b/src/web-ui/src/app/scenes/skills/SkillsScene.tsx @@ -2,30 +2,37 @@ import { Button, Checkbox, ConfirmDialog, + Empty, Field, Icon, IconButton, Input, + LoadingState, + NavigationPanel, + NavigationPanelBody, + NavigationPanelContent, + NavigationPanelHeader, NavigationPanelItem, + NavigationPanelSection, OverflowText, ScrollArea, SearchField, Select, StatusPill, Switch, - TabGroup, Dialog, DialogBody, DialogClose, DialogHeader, DialogHeading, DialogTitle, + type IconSource, } from '@openbitfun/ui'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { FolderOpen, Layers, Package, ShieldAlert, ShieldCheck, TrendingUp, Zap } from 'lucide-react'; +import { FolderOpen, Layers, Package, ShieldAlert, ShieldCheck, TrendingUp } from 'lucide-react'; import { useI18n } from '@/infrastructure/i18n/hooks/useI18n'; -import { GalleryDetailModal, GalleryPageHeader } from '@/app/components'; +import { GalleryDetailModal } from '@/app/components'; import type { SkillInfo, SkillLevel, SkillMarketItem } from '@/infrastructure/config/types'; import { installedSkillMarketIds, isSkillMarketItemInstalled } from '@/infrastructure/config/skillMarketInstallation'; import { @@ -45,9 +52,11 @@ import { getCardGradient } from '@/shared/utils/cardGradients'; import { useInstalledSkills } from './hooks/useInstalledSkills'; import { useSkillMarket } from './hooks/useSkillMarket'; import SkillCard from './components/SkillCard'; -import SkillsSuiteView from './components/SkillsSuiteView'; +import SkillGroupsView from './components/SkillGroupsView'; +import { useUserSkillGroups } from '@/features/skill-groups/useUserSkillGroups'; +import { resolveSkillGroups } from '@/features/skill-groups/skillGroups'; import './SkillsScene.scss'; -import { useSkillsSceneStore, type InstalledFilter } from './skillsSceneStore'; +import { useSkillsSceneStore, type SkillsView } from './skillsSceneStore'; import { useGallerySceneAutoRefresh } from '@/app/hooks/useGallerySceneAutoRefresh'; const log = createLogger('SkillsScene'); @@ -55,8 +64,8 @@ const log = createLogger('SkillsScene'); type SkillTab = 'installed' | 'discover'; interface CategoryInfo { - id: InstalledFilter; - icon: React.ReactNode; + id: SkillsView; + icon: IconSource; labelKey: string; titleKey: string; descKey: string; @@ -66,38 +75,38 @@ interface CategoryInfo { const CATEGORIES: CategoryInfo[] = [ { id: 'all', - icon: , + icon: { glyph: Layers }, labelKey: 'filters.all', titleKey: 'installed.titleListAll', descKey: 'categories.all', }, { id: 'builtin', - icon: , + icon: { glyph: ShieldCheck }, labelKey: 'filters.builtin', titleKey: 'installed.titleBuiltin', descKey: 'categories.builtin', }, { id: 'user', - icon: , + icon: { name: 'user' }, labelKey: 'filters.user', titleKey: 'installed.titleUser', descKey: 'categories.user', }, { id: 'project', - icon: , + icon: { glyph: FolderOpen }, labelKey: 'filters.project', titleKey: 'installed.titleProject', descKey: 'categories.project', }, { - id: 'suite', - icon: , - labelKey: 'filters.suite', - titleKey: 'suite.title', - descKey: 'categories.suite', + id: 'groups', + icon: { glyph: Layers }, + labelKey: 'filters.groups', + titleKey: 'groups.title', + descKey: 'categories.groups', }, ]; @@ -111,12 +120,12 @@ const SkillsScene: React.FC = () => { const { searchDraft, marketQuery, - installedFilter, + installedView, hideDuplicates, isAddFormOpen, setSearchDraft, submitMarketQuery, - setInstalledFilter, + setInstalledView, setHideDuplicates, setAddFormOpen, toggleAddForm, @@ -133,10 +142,22 @@ const SkillsScene: React.FC = () => { const installed = useInstalledSkills({ searchQuery: installedSearch, - activeFilter: installedFilter, + activeFilter: installedView === 'groups' ? 'all' : installedView, enabled: desktopConfigAvailable, }); + const skillGroups = useUserSkillGroups(desktopConfigAvailable); + const groupSkills = useMemo(() => installed.catalogReady ? installed.skills.map(skill => ({ + ...skill, + sourceLabel: getSkillSourceLabel(skill, t('list.item.unknownSource')), + runtimeStatus: installed.globallyDisabledSkillKeys.has(skill.key) + ? t('groups.globalDisabled') + : skill.isShadowed ? t('list.item.shadowed') : undefined, + })) : [], [installed.catalogReady, installed.skills, installed.globallyDisabledSkillKeys, t]); + const skillGroupCount = useMemo(() => resolveSkillGroups(groupSkills, skillGroups.groups, { + builtin: key => key, other: '', + }).length, [groupSkills, skillGroups.groups]); + const installedMarketIds = useMemo( () => installedSkillMarketIds(installed.skills), [installed.skills], @@ -206,8 +227,8 @@ const SkillsScene: React.FC = () => { }, [coverageSourceBySkillKey, installed.globallyDisabledSkillKeys, market.isRemoteWorkspace, t]); const refetchSkillsScene = useCallback(async () => { - await Promise.all([installed.loadSkills(true), market.refresh()]); - }, [installed, market]); + await Promise.all([installed.loadSkills(true), market.refresh(), skillGroups.reload()]); + }, [installed, market, skillGroups]); useGallerySceneAutoRefresh({ sceneId: 'skills', @@ -246,130 +267,192 @@ const SkillsScene: React.FC = () => { return list; }, [hideDuplicates, installed.filteredSkills]); - const installedCategories: CategoryInfo[] = [ - ...CATEGORIES.filter((category) => category.id !== 'suite'), - ...installed.sourceGroups.map((group) => ({ - id: group.id, - icon: , - labelKey: 'filters.source', - titleKey: 'installed.titleSource', - descKey: 'categories.source', - sourceLabel: group.label, - })), - ...CATEGORIES.filter((category) => category.id === 'suite'), + const installedLoadFailed = Boolean(installed.error) + || (installed.skills.length === 0 && installed.diagnostics.length > 0); + + const sourceCategories: CategoryInfo[] = installed.sourceGroups.map((group) => ({ + id: group.id, + icon: { name: 'extension' }, + labelKey: 'filters.source', + titleKey: 'installed.titleSource', + descKey: 'categories.source', + sourceLabel: group.label, + })); + const installedCategories = [...CATEGORIES, ...sourceCategories]; + const categorySections = [ + { title: t('nav.categories.installed'), categories: CATEGORIES }, + { title: t('list.columns.source'), categories: sourceCategories }, ]; - const activeInstalledCategory = installedCategories.find((category) => category.id === installedFilter) + const activeInstalledCategory = installedCategories.find((category) => category.id === installedView) ?? CATEGORIES[0]; useEffect(() => { - if (!installed.loading && !installed.error && installedFilter.startsWith('source:') - && !installed.sourceGroups.some((group) => group.id === installedFilter)) { - setInstalledFilter('all'); + if (!installed.loading && !installed.error && installedView.startsWith('source:') + && !installed.sourceGroups.some((group) => group.id === installedView)) { + setInstalledView('all'); } - }, [installed.loading, installed.error, installed.sourceGroups, installedFilter, setInstalledFilter]); + }, [installed.loading, installed.error, installed.sourceGroups, installedView, setInstalledView]); + + const installedListHeader = ( +
    +
    + {t('nav.title')} + {!installed.loading && ( + + {formatNumber(installedFiltered.length)} + + )} +
    + {t('list.columns.source')} + {t('list.columns.status')} + {t('list.columns.actions')} +
    + ); return (
    - } - onClick={toggleAddForm} - data-testid="skills-add-skill-btn" - > - {t('toolbar.addTooltip')} - - ) : undefined} - /> -
    -
    - setActiveTab(value as SkillTab)} - aria-label={t('nav.title')} - items={(['installed', 'discover'] as const).map((tab) => ({ - value: tab, - id: `skills-tab-${tab}`, - panelId: `skills-panel-${tab}`, - disabled: tab === 'discover' && !desktopConfigAvailable, - icon: , - label: ( - + +
    +
    {t('nav.title')}
    +
    +
    + { setName(event.target.value); setError(null); }} /> +
    + )} +
    + setQuery('') : undefined} /> + {!draft.readOnly && ( + + )} +
    + {t('groups.memberCount', { count: selected.size })} + {!catalogReady &&

    {t('groups.catalogUnavailable')}

    } + + {visible.map(({ key, skill }) => { + const title = ( + {skill?.name ?? key} + ); + const details = skill + ? [skill.sourceLabel ?? skill.sourceSlot, skill.description, skill.runtimeStatus].filter(Boolean).join(' · ') + : t(catalogReady ? 'groups.unavailableMember' : 'groups.unresolvedMember'); + return ( +
    + {draft.readOnly ? ( +
    + {title} + {details} +
    + ) : ( + { + setSkillKeys(current => checked ? [...new Set([...current, key])] : current.filter(item => item !== key)); + setError(null); + }} /> + )} +
    + ); + })} + {visible.length === 0 &&

    {t('groups.noMatchingSkills')}

    } +
    + {error && } +
    + + {draft.readOnly ? ( + + ) : ( + + )} +
    + + + + ); +} diff --git a/src/web-ui/src/app/scenes/skills/components/SkillGroupsView.scss b/src/web-ui/src/app/scenes/skills/components/SkillGroupsView.scss new file mode 100644 index 0000000000..93f43275eb --- /dev/null +++ b/src/web-ui/src/app/scenes/skills/components/SkillGroupsView.scss @@ -0,0 +1,238 @@ +@use './SkillsList' as skills-list; + +.skill-groups { + --skill-groups-inline-inset: var(--openbitfun-space-6); + display: flex; + flex: 1; + min-block-size: 0; + flex-direction: column; + overflow: hidden; +} + +.skill-groups__sections { + flex: 1; + min-block-size: 0; + display: flex; + flex-direction: column; + gap: var(--openbitfun-space-6); + // Keep the toolbar gap outside the viewport so sticky headers meet its clip edge. + margin-block-start: var(--openbitfun-space-2); + padding-block: 0 var(--openbitfun-space-6); + padding-inline: var(--skill-groups-inline-inset) calc(var(--skill-groups-inline-inset) + var(--openbitfun-space-1)); +} + +.skill-groups__section { + flex: none; + min-inline-size: 0; +} + +.skill-groups__section-title { + min-inline-size: 0; + margin: 0; + font: inherit; +} + +.skill-groups__list { + @include skills-list.surface; + // Keep rounded clipping without taking scroll ownership away from ScrollArea. + overflow: clip; +} + +.skill-groups__list-header, +.skill-groups__row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(120px, 0.4fr) 96px; + grid-template-areas: 'identity members actions'; + column-gap: var(--openbitfun-space-4); + min-inline-size: 0; +} + +.skill-groups__list-header { + @include skills-list.header; + position: sticky; + inset-block-start: 0; +} + +.skill-groups__list-heading { + display: flex; + align-items: center; + gap: var(--openbitfun-space-2); + min-inline-size: 0; +} + +.skill-groups__count { + @include skills-list.count; + flex: none; +} + +.skill-groups__column-label { + white-space: nowrap; + + &--actions { + text-align: end; + } +} + +.skill-groups__row { + @include skills-list.row; +} + +.skill-groups__open { + @include skills-list.open-target; +} + +.skill-groups__identity, +.skill-groups__meta { + display: flex; + flex-direction: column; + gap: var(--openbitfun-space-1); + min-inline-size: 0; +} + +.skill-groups__identity { + grid-area: identity; +} + +.skill-groups__name { + color: var(--openbitfun-color-content-primary); + font-size: var(--openbitfun-type-label-md-font-size); + font-weight: var(--openbitfun-type-label-selected-font-weight); + line-height: var(--openbitfun-type-label-md-line-height); +} + +.skill-groups__preview, +.skill-groups__meta, +.skill-groups__empty, +.skill-groups__notice, +.skill-group-editor__meta { + color: var(--openbitfun-color-content-muted); + font-size: var(--openbitfun-type-label-sm-font-size); +} + +.skill-groups__meta { + grid-area: members; + align-items: flex-start; + font-variant-numeric: tabular-nums; +} + +.skill-groups__actions { + grid-area: actions; + position: relative; + z-index: 2; + display: flex; + justify-content: flex-end; + min-inline-size: 0; +} + +.skill-groups__empty { + padding: var(--openbitfun-space-4); +} + +.skill-groups__notice { + margin: var(--openbitfun-space-2) var(--skill-groups-inline-inset); +} + +.skill-group-editor { + display: flex; + min-block-size: 0; + flex-direction: column; + gap: var(--openbitfun-space-3); +} + +.skill-group-editor__field, +.skill-group-editor__identity { + display: flex; + min-inline-size: 0; + flex-direction: column; + gap: var(--openbitfun-space-1); + font-size: var(--openbitfun-type-label-md-font-size); +} + +.skill-group-editor__toolbar, +.skill-group-editor__footer { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--openbitfun-space-3); +} + +.skill-group-editor__members { + min-block-size: 80px; + max-block-size: min(360px, 42vh); + border-block: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); +} + +.skill-group-editor__member { + padding-block: var(--openbitfun-space-3); + color: var(--openbitfun-color-content-primary); + overflow-wrap: anywhere; + + & + & { + border-block-start: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); + } +} + +.skill-group-editor__selection { + inline-size: 100%; +} + +.skill-group-editor__member-title { + font-family: var(--openbitfun-type-heading-card-font-family); + font-size: var(--openbitfun-type-heading-card-font-size); + font-weight: var(--openbitfun-type-heading-card-font-weight); + line-height: var(--openbitfun-type-heading-card-line-height); + letter-spacing: var(--openbitfun-type-heading-card-letter-spacing); +} + +.skill-group-editor__member-description { + color: var(--openbitfun-color-content-muted); + font-family: var(--openbitfun-type-support-font-family); + font-size: var(--openbitfun-type-support-font-size); + font-weight: var(--openbitfun-type-support-font-weight); + line-height: var(--openbitfun-type-support-line-height); + letter-spacing: var(--openbitfun-type-support-letter-spacing); +} + +.skill-group-editor__error { + margin: 0; + font-size: var(--openbitfun-type-label-sm-font-size); +} + +.skill-group-editor__footer { + justify-content: flex-end; +} + +@container skills-content (max-width: 680px) { + .skill-groups { + --skill-groups-inline-inset: var(--openbitfun-space-4); + } + + .skill-groups__list-header { + display: flex; + } + + .skill-groups__column-label { + position: absolute; + inline-size: 1px; + block-size: 1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + } + + .skill-groups__row { + grid-template-columns: minmax(0, 1fr) 96px; + grid-template-areas: + 'identity actions' + 'members members'; + row-gap: var(--openbitfun-space-2); + } +} + +@container skills-content (max-width: 480px) { + .skill-groups__row { + grid-template-areas: + 'identity identity' + 'members actions'; + } +} diff --git a/src/web-ui/src/app/scenes/skills/components/SkillGroupsView.tsx b/src/web-ui/src/app/scenes/skills/components/SkillGroupsView.tsx new file mode 100644 index 0000000000..da8983e3a3 --- /dev/null +++ b/src/web-ui/src/app/scenes/skills/components/SkillGroupsView.tsx @@ -0,0 +1,205 @@ +import { useMemo, useRef, useState } from 'react'; +import { + Button, Icon, IconButton, LoadingState, MenuPopover, OverflowText, ScrollArea, SearchField, + type MenuEntry, +} from '@openbitfun/ui'; +import { useI18n } from '@/infrastructure/i18n/hooks/useI18n'; +import { confirmDialog } from '@/infrastructure/confirm-dialog'; +import { isSurfaceChangedError } from '@/infrastructure/peer-device/deviceSurface'; +import type { UserSkillGroup } from '@/infrastructure/config/types'; +import { + builtinSkillGroupLabelKey, deleteUserSkillGroup, moveUserSkillGroup, + resolveSkillGroups, saveUserSkillGroup, type GroupableSkill, type ResolvedSkillGroup, +} from '@/features/skill-groups/skillGroups'; +import type { useUserSkillGroups } from '@/features/skill-groups/useUserSkillGroups'; +import { SkillGroupEditor, type SkillGroupDraft } from './SkillGroupEditor'; +import { skillGroupErrorMessage } from './skillGroupMessages'; +import './SkillGroupsView.scss'; + +interface SkillGroupsViewProps { + skills: GroupableSkill[]; + collection: ReturnType; + catalogReady: boolean; + catalogLoading: boolean; + catalogIncomplete: boolean; + onRefresh: () => void; +} + +function GroupActions({ label, items, disabled }: { label: string; items: MenuEntry[]; disabled: boolean }) { + const anchorRef = useRef(null); + const [open, setOpen] = useState(false); + return ( + <> + } + onClick={() => setOpen(value => !value)} /> + setOpen(false)} + items={items} placement="bottom" aria-label={label} /> + + ); +} + +export default function SkillGroupsView({ skills, collection, catalogReady, catalogLoading, catalogIncomplete, onRefresh }: SkillGroupsViewProps) { + const { t, formatNumber } = useI18n('scenes/skills'); + const { t: tComponents } = useI18n('components'); + const [query, setQuery] = useState(''); + const [draft, setDraft] = useState(null); + const [actionError, setActionError] = useState(null); + const groups = useMemo(() => resolveSkillGroups(skills, collection.groups, { + builtin: key => { + const labelKey = builtinSkillGroupLabelKey(key); + return labelKey ? t(`groups.builtin.${labelKey}`) : key; + }, + other: '', + }), [collection.groups, skills, t]); + const normalizedQuery = query.trim().toLowerCase(); + const filtered = groups.filter(group => !normalizedQuery || [group.label, ...group.skills.map(skill => skill.name)] + .some(value => value.toLowerCase().includes(normalizedQuery))); + const userGroups = filtered.filter(group => group.kind === 'user'); + const builtinGroups = filtered.filter(group => group.kind === 'builtin'); + const busy = collection.saving || !collection.ready; + + const copy = (group: ResolvedSkillGroup | UserSkillGroup) => { + const label = 'label' in group ? group.label : group.name; + const baseName = t('groups.copyName', { name: label }); + let name = baseName; + let index = 2; + while (collection.groups.some(item => item.name.toLowerCase() === name.toLowerCase())) { + name = t('groups.copyNameNumbered', { name: baseName, count: index++ }); + } + setDraft({ group: { id: crypto.randomUUID(), name, skillKeys: [...group.skillKeys] }, original: null }); + }; + + const edit = (group: ResolvedSkillGroup) => { + setActionError(null); + const original = collection.groups.find(item => `user:${item.id}` === group.id); + setDraft(original + ? { group: { ...original, skillKeys: [...original.skillKeys] }, original } + : { group: { id: group.id, name: group.label, skillKeys: group.skillKeys }, original: null, readOnly: true }); + }; + + const mutate = async (change: (groups: UserSkillGroup[]) => UserSkillGroup[]) => { + setActionError(null); + try { + await collection.updateGroups(change); + } catch (error) { + if (!isSurfaceChangedError(error)) setActionError(skillGroupErrorMessage(error, t, 'save')); + } + }; + + const remove = async (group: UserSkillGroup) => { + const confirmed = await confirmDialog({ + title: t('groups.deleteTitle'), message: t('groups.deleteMessage', { name: group.name }), + confirmText: t('groups.delete'), cancelText: t('groups.cancel'), confirmDanger: true, type: 'warning', + }); + if (confirmed && collection.scope.isCurrent()) await mutate(current => deleteUserSkillGroup(current, group)); + }; + + const renderGroup = (group: ResolvedSkillGroup) => { + const original = collection.groups.find(item => `user:${item.id}` === group.id); + const index = collection.groups.findIndex(item => item.id === original?.id); + const items: MenuEntry[] = [ + { id: 'edit', label: t('groups.edit'), onSelect: () => edit(group) }, + { id: 'copy', label: t('groups.copy'), onSelect: () => copy(group) }, + { id: 'up', label: t('groups.moveUp'), disabled: index <= 0, + onSelect: () => { if (original) void mutate(current => moveUserSkillGroup(current, original.id, -1)); } }, + { id: 'down', label: t('groups.moveDown'), disabled: index === collection.groups.length - 1, + onSelect: () => { if (original) void mutate(current => moveUserSkillGroup(current, original.id, 1)); } }, + { id: 'delete', label: t('groups.delete'), tone: 'danger', + onSelect: () => { if (original) void remove(original); } }, + ]; + return ( +
    +
    +
    +
    + {t('groups.memberCount', { count: group.skillKeys.length })} + {catalogReady && !catalogIncomplete && group.unavailableSkillKeys.length > 0 && ( + {t('groups.unavailableCount', { count: group.unavailableSkillKeys.length })} + )} +
    +
    + {original + ? + : } +
    +
    + ); + }; + + const renderSection = (label: string, sectionGroups: ResolvedSkillGroup[]) => ( +
    +
    +
    +
    +

    {label}

    + {formatNumber(sectionGroups.length)} +
    + {t('nav.title')} + {t('list.columns.actions')} +
    +
    + {sectionGroups.length ? sectionGroups.map(renderGroup) : ( +
    +
    + {t(normalizedQuery ? 'groups.noMatch' : 'groups.noGroups')} +
    +
    + )} +
    +
    +
    + ); + + return ( +
    +
    +
    +
    +

    {t('groups.title')}

    +

    {t('groups.subtitle')}

    +
    +
    + +
    +
    + setQuery('') : undefined} /> + } + size="sm" disabled={collection.saving || collection.loading || catalogLoading} onClick={onRefresh} /> +
    + {collection.loading && {t('groups.loading')}} + {collection.error ? ( +

    {skillGroupErrorMessage(collection.error, t, 'load')}

    + ) : null} + {actionError &&

    {actionError}

    } + {!catalogLoading && (!catalogReady || catalogIncomplete) && ( +

    {t('groups.catalogUnavailable')}

    + )} + {collection.ready && ( + + {renderSection(t('groups.myGroups'), userGroups)} + {builtinGroups.length > 0 && renderSection(t('groups.builtinGroups'), builtinGroups)} + + )} + {draft && ( + setDraft(null)} onCopy={() => copy(draft.group)} + onSave={group => collection.updateGroups(current => saveUserSkillGroup(current, group, draft.original))} /> + )} +
    + ); +} diff --git a/src/web-ui/src/app/scenes/skills/components/SkillsSuiteView.tsx b/src/web-ui/src/app/scenes/skills/components/SkillsSuiteView.tsx deleted file mode 100644 index cd80cb3abb..0000000000 --- a/src/web-ui/src/app/scenes/skills/components/SkillsSuiteView.tsx +++ /dev/null @@ -1,663 +0,0 @@ -import { Button, Checkbox, Icon, IconButton, OverflowText, ScrollArea, SegmentedControl, StatusPill } from '@openbitfun/ui'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Package, RotateCcw } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; -import { confirmDialog } from '@/infrastructure/confirm-dialog'; -import { configAPI } from '@/infrastructure/api'; -import { useWorkspaceManagerSync } from '@/infrastructure/hooks/useWorkspaceManagerSync'; -import { useGallerySceneAutoRefresh } from '@/app/hooks/useGallerySceneAutoRefresh'; -import type { ModeSkillInfo } from '@/infrastructure/config/types'; -import { buildSkillCoverageSourceMap } from '@/infrastructure/config/skillSourcePresentation'; -import { useNotification } from '@/shared/notification-system'; -import { createLogger } from '@/shared/utils/logger'; -import { SkillGroupManagerModal } from '../../agents/components/SkillGroupPicker'; -import { - type ResolvedSkillGroupKind, - builtinSkillGroupLabelKey, - resolveSkillGroups, -} from '../../agents/components/skillGroups'; -import { useUserSkillGroups } from '../../agents/components/useUserSkillGroups'; -import type { SuiteModeId } from '../skillsSceneStore'; -import { useSkillsSceneStore } from '../skillsSceneStore'; - -const log = createLogger('SkillsSuiteView'); - -const SUITE_MODES = [ - { id: 'agentic', labelKey: 'suite.modes.agentic', descKey: 'suite.modeDescriptions.agentic' }, - { id: 'Cowork', labelKey: 'suite.modes.cowork', descKey: 'suite.modeDescriptions.cowork' }, - { id: 'Claw', labelKey: 'shared:agents.claw', descKey: 'suite.modeDescriptions.claw' }, -] as const; - -type SuiteMode = typeof SUITE_MODES[number]; - -interface SuiteSkillGroup { - id: string; - kind: ResolvedSkillGroupKind; - label: string; - skills: ModeSkillInfo[]; - enabledCount: number; - totalCount: number; -} - -type SavingAction = { - groupKey: string; - kind: 'save' | 'toggle'; -} | null; - -function uniqueKeys(keys: Iterable): string[] { - return [...new Set([...keys].filter(Boolean))]; -} - -function buildEnabledKeySet(skills: ModeSkillInfo[]): string[] { - return uniqueKeys(skills.filter((skill) => skill.effectiveEnabled).map((skill) => skill.key)); -} - -function cloneSet(keys: Iterable): Set { - return new Set(keys); -} - -function buildSuiteSkillGroups( - skills: ModeSkillInfo[], - userGroups: Parameters[1], - enabledKeySet: Set, - t: (key: string) => string, -): SuiteSkillGroup[] { - return resolveSkillGroups(skills, userGroups, { - builtin: (groupKey) => { - const labelKey = builtinSkillGroupLabelKey(groupKey); - return labelKey ? t(`suite.groups.${labelKey}`) : groupKey; - }, - other: t('suite.groups.other'), - }).map((group) => { - const groupSkills = group.skills as ModeSkillInfo[]; - return { - id: group.id, - kind: group.kind, - label: group.label, - skills: groupSkills.sort((left, right) => { - const leftEnabled = enabledKeySet.has(left.key); - const rightEnabled = enabledKeySet.has(right.key); - if (leftEnabled && !rightEnabled) return -1; - if (!leftEnabled && rightEnabled) return 1; - return left.name.localeCompare(right.name) || left.key.localeCompare(right.key); - }), - enabledCount: groupSkills.filter((skill) => enabledKeySet.has(skill.key)).length, - totalCount: groupSkills.length, - }; - }); -} - -function groupSectionLabel(kind: ResolvedSkillGroupKind, t: (key: string) => string): string { - switch (kind) { - case 'user': - return t('suite.sections.myGroups'); - case 'builtin': - return t('suite.sections.builtin'); - default: - return t('suite.sections.otherSkills'); - } -} - -function buildSkillTitle( - skill: ModeSkillInfo, - enabled: boolean, - shadowed: boolean, - dirty: boolean, - coverageSource: string, - t: (key: string, options?: { source: string }) => string, -): string { - return [ - skill.description || skill.name, - !skill.globallyEnabled - ? t('suite.skillState.globalDisabled') - : dirty - ? t('suite.skillState.pending') - : enabled - ? t('suite.skillState.enabled') - : t('suite.skillState.disabled'), - shadowed ? t('suite.skillState.coveredDetail', { source: coverageSource }) : null, - ].filter(Boolean).join('\n'); -} - -function buildGroupKeySet(group: SuiteSkillGroup): Set { - return new Set(group.skills.map((skill) => skill.key)); -} - -function isSameKeySet(leftKeys: string[], rightKeys: string[]): boolean { - if (leftKeys.length !== rightKeys.length) { - return false; - } - - const rightKeySet = new Set(rightKeys); - return leftKeys.every((key) => rightKeySet.has(key)); -} - -const SkillsSuiteView: React.FC = () => { - const { t } = useTranslation('scenes/skills'); - const notification = useNotification(); - const { workspacePath } = useWorkspaceManagerSync(); - const suiteModeId = useSkillsSceneStore((state) => state.suiteModeId); - const setSuiteModeId = useSkillsSceneStore((state) => state.setSuiteModeId); - const { - groups: userSkillGroups, - saveGroups: saveUserSkillGroups, - } = useUserSkillGroups(); - - const [modeSkills, setModeSkills] = useState([]); - const [committedEnabledKeys, setCommittedEnabledKeys] = useState([]); - const [draftEnabledKeys, setDraftEnabledKeys] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [savingAction, setSavingAction] = useState(null); - const [resettingModeId, setResettingModeId] = useState(null); - const [isGroupManagerOpen, setIsGroupManagerOpen] = useState(false); - const loadRequestIdRef = useRef(0); - - const currentMode = useMemo( - () => SUITE_MODES.find((mode) => mode.id === suiteModeId) ?? SUITE_MODES[0], - [suiteModeId], - ); - - const committedEnabledKeySet = useMemo( - () => cloneSet(committedEnabledKeys), - [committedEnabledKeys], - ); - const draftEnabledKeySet = useMemo( - () => cloneSet(draftEnabledKeys), - [draftEnabledKeys], - ); - const coverageSourceBySkillKey = useMemo( - () => buildSkillCoverageSourceMap(modeSkills, t('list.item.unknownSource')), - [modeSkills, t], - ); - - const suiteGroups = useMemo( - () => buildSuiteSkillGroups(modeSkills, userSkillGroups, draftEnabledKeySet, t), - [modeSkills, userSkillGroups, draftEnabledKeySet, t], - ); - const suiteSections = useMemo(() => { - const sections = new Map(); - for (const group of suiteGroups) { - const label = groupSectionLabel(group.kind, t); - const groups = sections.get(label) ?? []; - groups.push(group); - sections.set(label, groups); - } - return [...sections.entries()]; - }, [suiteGroups, t]); - - const hasUnsavedChanges = useMemo( - () => !isSameKeySet(draftEnabledKeys, committedEnabledKeys), - [committedEnabledKeys, draftEnabledKeys], - ); - - const isSaving = savingAction !== null || resettingModeId !== null; - - const loadModeSkills = useCallback(async (forceRefresh?: boolean) => { - const requestId = ++loadRequestIdRef.current; - - try { - setLoading(true); - setError(null); - const skills = await configAPI.getModeSkillConfigs({ - modeId: suiteModeId, - forceRefresh, - workspacePath: workspacePath || undefined, - }); - - if (requestId !== loadRequestIdRef.current) { - return; - } - - setModeSkills(skills); - const enabledKeys = buildEnabledKeySet(skills); - setCommittedEnabledKeys(enabledKeys); - setDraftEnabledKeys(enabledKeys); - } catch (loadError) { - if (requestId !== loadRequestIdRef.current) { - return; - } - - const message = loadError instanceof Error ? loadError.message : String(loadError); - log.error('Failed to load skill suite mode configs', { - modeId: suiteModeId, - workspacePath, - error: loadError, - }); - setError(message); - } finally { - if (requestId === loadRequestIdRef.current) { - setLoading(false); - } - } - }, [suiteModeId, workspacePath]); - - useEffect(() => { - void loadModeSkills(); - }, [loadModeSkills]); - - useGallerySceneAutoRefresh({ - sceneId: 'skills', - refetch: () => loadModeSkills(true), - enabled: !hasUnsavedChanges, - }); - - const refresh = useCallback(async () => { - if (hasUnsavedChanges) { - notification.warning(t('suite.messages.saveFirst')); - return; - } - try { - await loadModeSkills(true); - } catch (refreshError) { - notification.error( - t('suite.messages.refreshFailed', { - error: refreshError instanceof Error ? refreshError.message : String(refreshError), - }), - ); - } - }, [hasUnsavedChanges, loadModeSkills, notification, t]); - - const handleModeSelect = useCallback((modeId: typeof SUITE_MODES[number]['id']) => { - if (hasUnsavedChanges) { - notification.warning(t('suite.messages.saveFirst')); - return; - } - - setSuiteModeId(modeId); - }, [hasUnsavedChanges, notification, setSuiteModeId, t]); - - const resetMode = useCallback(async (mode: SuiteMode) => { - const shouldReset = await confirmDialog({ - title: t('suite.resetDialog.title', { mode: t(mode.labelKey) }), - message: t( - mode.id === suiteModeId && hasUnsavedChanges - ? 'suite.resetDialog.messageWithUnsaved' - : 'suite.resetDialog.message', - { mode: t(mode.labelKey) }, - ), - confirmText: t('suite.resetDialog.confirm'), - cancelText: t('suite.resetDialog.cancel'), - confirmDanger: true, - type: 'warning', - }); - - if (!shouldReset) { - return; - } - - setResettingModeId(mode.id); - - try { - await configAPI.resetModeSkillSelection({ - modeId: mode.id, - workspacePath: workspacePath || undefined, - }); - - if (mode.id === suiteModeId) { - await loadModeSkills(true); - } - - const { globalEventBus } = await import('@/infrastructure/event-bus'); - globalEventBus.emit('mode:config:updated'); - notification.success(t('suite.messages.resetSuccess', { mode: t(mode.labelKey) })); - } catch (resetError) { - log.error('Failed to reset skill suite visibility', { - modeId: mode.id, - workspacePath, - error: resetError, - }); - notification.error(t('suite.messages.resetFailed', { - error: resetError instanceof Error ? resetError.message : String(resetError), - })); - } finally { - setResettingModeId(null); - } - }, [hasUnsavedChanges, loadModeSkills, notification, suiteModeId, t, workspacePath]); - - const saveGroup = useCallback(async (group: SuiteSkillGroup) => { - setSavingAction({ groupKey: group.id, kind: 'save' }); - const nextCommitted = uniqueKeys(draftEnabledKeys); - - try { - await configAPI.replaceModeSkillSelection({ - modeId: suiteModeId, - enabledSkillKeys: nextCommitted, - workspacePath: workspacePath || undefined, - }); - - const refreshedSkills = await configAPI.getModeSkillConfigs({ - modeId: suiteModeId, - forceRefresh: true, - workspacePath: workspacePath || undefined, - }); - setModeSkills(refreshedSkills); - const refreshedEnabledKeys = buildEnabledKeySet(refreshedSkills); - setCommittedEnabledKeys(refreshedEnabledKeys); - setDraftEnabledKeys(refreshedEnabledKeys); - - const { globalEventBus } = await import('@/infrastructure/event-bus'); - globalEventBus.emit('mode:config:updated'); - - notification.success( - t('suite.messages.saveSuccess', { - mode: t(currentMode.labelKey), - }), - ); - } catch (saveError) { - log.error('Failed to update skill suite visibility', { - modeId: suiteModeId, - groupKey: group.id, - workspacePath, - error: saveError, - }); - notification.error( - t('suite.messages.saveFailed', { - error: saveError instanceof Error ? saveError.message : String(saveError), - }), - ); - } finally { - setSavingAction(null); - } - }, [currentMode.labelKey, draftEnabledKeys, notification, suiteModeId, t, workspacePath]); - - const saveGroupVisibility = useCallback(async (group: SuiteSkillGroup, enabled: boolean) => { - const groupKeys = buildGroupKeySet(group); - const previousDraft = draftEnabledKeys; - const baseDraft = draftEnabledKeys.filter((key) => !groupKeys.has(key)); - const finalDraft = enabled - ? uniqueKeys([...baseDraft, ...group.skills.map((skill) => skill.key)]) - : uniqueKeys(baseDraft); - setSavingAction({ groupKey: group.id, kind: 'toggle' }); - setDraftEnabledKeys(finalDraft); - try { - await configAPI.replaceModeSkillSelection({ - modeId: suiteModeId, - enabledSkillKeys: finalDraft, - workspacePath: workspacePath || undefined, - }); - const refreshedSkills = await configAPI.getModeSkillConfigs({ - modeId: suiteModeId, - forceRefresh: true, - workspacePath: workspacePath || undefined, - }); - setModeSkills(refreshedSkills); - const refreshedEnabledKeys = buildEnabledKeySet(refreshedSkills); - setCommittedEnabledKeys(refreshedEnabledKeys); - setDraftEnabledKeys(refreshedEnabledKeys); - const { globalEventBus } = await import('@/infrastructure/event-bus'); - globalEventBus.emit('mode:config:updated'); - notification.success(t('suite.messages.saveSuccess', { mode: t(currentMode.labelKey) })); - } catch (saveError) { - log.error('Failed to update skill suite visibility', { - modeId: suiteModeId, - groupKey: group.id, - workspacePath, - error: saveError, - }); - notification.error(t('suite.messages.saveFailed', { - error: saveError instanceof Error ? saveError.message : String(saveError), - })); - setDraftEnabledKeys(previousDraft); - } finally { - setSavingAction(null); - } - }, [currentMode.labelKey, draftEnabledKeys, notification, suiteModeId, t, workspacePath]); - - return ( -
    -
    -
    -

    {t('suite.title')}

    -

    {t('suite.subtitle')}

    -
    -
    - - -
    -
    - -
    -
    - handleModeSelect(value as SuiteModeId)} - aria-label={t('suite.modeLabel')} - options={SUITE_MODES.map((mode) => ({ - value: mode.id, - label: ( - - {t(mode.labelKey)} - - ), - }))} - /> -
    - { void resetMode(currentMode); }} - title={t('suite.modeActions.reset', { mode: t(currentMode.labelKey) })} - aria-label={t('suite.modeActions.reset', { mode: t(currentMode.labelKey) })} - icon={} - /> -
    - - {loading && ( -
    - - {t('suite.loading')} -
    - )} - - {!loading && error && ( -
    - - {error} -
    - )} - - {!loading && !error && suiteGroups.length === 0 && ( -
    - - {t('suite.empty')} -
    - )} - - {!loading && !error && suiteGroups.length > 0 && ( - - {suiteSections.map(([sectionLabel, sectionGroups]) => ( -
    - {sectionLabel} -
    - {sectionGroups.map((group) => { - const allEnabled = group.enabledCount === group.totalCount; - const someEnabled = group.enabledCount > 0; - const groupDirty = group.skills.some( - (skill) => committedEnabledKeySet.has(skill.key) !== draftEnabledKeySet.has(skill.key), - ); - const showSaveButton = groupDirty - && !(savingAction?.groupKey === group.id && savingAction.kind === 'toggle'); - const groupStateVariant = allEnabled ? 'success' : someEnabled ? 'warning' : 'neutral'; - const groupStateLabel = allEnabled - ? t('suite.groupState.enabled') - : someEnabled - ? t('suite.groupState.partial') - : t('suite.groupState.disabled'); - - return ( -
    -
    -
    -
    - {group.label} - {groupStateLabel} -
    - - {t('suite.groupCount', { total: group.totalCount })} - -
    - -
    - {showSaveButton ? ( - - ) : null} - -
    -
    - -
    - {group.skills.map((skill) => { - const draftEnabled = draftEnabledKeySet.has(skill.key); - const dirty = committedEnabledKeySet.has(skill.key) !== draftEnabled; - const coverageSource = coverageSourceBySkillKey.get(skill.key) - ?? t('list.item.unknownSource'); - const shadowed = draftEnabled && !dirty && coverageSourceBySkillKey.has(skill.key); - const accessibleStatus = buildSkillTitle( - skill, - draftEnabled, - shadowed, - dirty, - coverageSource, - t, - ); - - return ( -
    - - {skill.name} - - )} - onCheckedChange={(checked) => { - setDraftEnabledKeys((prev) => { - const next = new Set(prev); - if (checked) { - next.add(skill.key); - } else { - next.delete(skill.key); - } - return uniqueKeys(next); - }); - }} - > - {shadowed && ( - - {t('suite.skillState.covered', { source: coverageSource })} - - )} - {dirty && ( - - {t('suite.skillState.pending')} - - )} - {!skill.globallyEnabled && ( - - {t('suite.skillState.globalDisabled')} - - )} - -
    - ); - })} -
    -
    - ); - })} -
    -
    - ))} -
    - )} - setIsGroupManagerOpen(false)} - skills={modeSkills} - groups={userSkillGroups} - onSaveGroups={saveUserSkillGroups} - /> -
    - ); -}; - -export default SkillsSuiteView; diff --git a/src/web-ui/src/app/scenes/skills/components/_SkillsList.scss b/src/web-ui/src/app/scenes/skills/components/_SkillsList.scss new file mode 100644 index 0000000000..17fb036a15 --- /dev/null +++ b/src/web-ui/src/app/scenes/skills/components/_SkillsList.scss @@ -0,0 +1,84 @@ +// Shared list presentation for installed skills and reusable skill groups. +// Each collection owns its columns and scroll region. +@mixin surface { + display: flex; + flex-direction: column; + overflow: hidden; + border: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); + border-radius: var(--openbitfun-radius-lg); + background: transparent; + isolation: isolate; +} + +@mixin header { + position: relative; + z-index: 2; + flex-shrink: 0; + align-items: center; + padding: var(--openbitfun-space-3) var(--openbitfun-space-4); + border-block-end: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); + background: var(--openbitfun-color-surface-tertiary); + color: var(--openbitfun-color-content-muted); + font-size: var(--openbitfun-type-meta-font-size); + font-weight: var(--openbitfun-type-meta-font-weight); + line-height: var(--openbitfun-type-meta-line-height); +} + +@mixin count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + border-radius: var(--openbitfun-radius-pill); + background: var(--openbitfun-color-action-neutral-surface-hover); + color: var(--openbitfun-color-content-muted); + font-size: var(--openbitfun-type-micro-font-size); + font-weight: var(--openbitfun-type-label-sm-font-weight); +} + +@mixin row { + position: relative; + isolation: isolate; + flex-shrink: 0; + align-items: center; + min-block-size: 88px; + padding: var(--openbitfun-space-4); + border-block-end: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); + background: transparent; + transition: background-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); + + &:last-child { + border-block-end: 0; + } + + &:not([aria-disabled='true']):is(:hover, :focus-within) { + background: var(--openbitfun-color-action-neutral-surface-hover); + } + + @media (prefers-reduced-motion: reduce) { + transition: none; + } +} + +@mixin open-target { + position: absolute; + inset: 0; + z-index: 1; + inline-size: 100%; + block-size: 100%; + padding: 0; + border: 0; + background: transparent; + cursor: pointer; + + &:focus-visible { + outline: var(--openbitfun-focus-width) solid var(--openbitfun-color-focus-ring); + outline-offset: calc(-1 * var(--openbitfun-focus-width)); + } + + &:disabled { + cursor: default; + } +} diff --git a/src/web-ui/src/app/scenes/skills/components/skillGroupMessages.ts b/src/web-ui/src/app/scenes/skills/components/skillGroupMessages.ts new file mode 100644 index 0000000000..90bece0d0c --- /dev/null +++ b/src/web-ui/src/app/scenes/skills/components/skillGroupMessages.ts @@ -0,0 +1,16 @@ +import type { UseI18nReturn } from '@/infrastructure/i18n/hooks/useI18n'; +import { SkillGroupError } from '@/features/skill-groups/skillGroups'; + +export function skillGroupErrorMessage(error: unknown, t: UseI18nReturn['t'], operation: 'load' | 'save') { + if (error instanceof SkillGroupError) { + switch (error.code) { + case 'invalidConfig': return t('groups.errors.invalidConfig'); + case 'unsupportedVersion': return t('groups.errors.unsupportedVersion'); + case 'nameRequired': return t('groups.errors.nameRequired'); + case 'nameDuplicate': return t('groups.errors.nameDuplicate'); + case 'conflict': return t('groups.errors.conflict'); + case 'notReady': return t('groups.errors.notReady'); + } + } + return t(operation === 'load' ? 'groups.errors.loadFailed' : 'groups.errors.saveFailed'); +} diff --git a/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.test.tsx b/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.test.tsx index 3c2e9e2c55..bf12275088 100644 --- a/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.test.tsx +++ b/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.test.tsx @@ -8,7 +8,11 @@ import { useInstalledSkills } from './useInstalledSkills'; import type { InstalledFilter } from '../skillsSceneStore'; const getSkillConfigsMock = vi.hoisted(() => vi.fn()); -const diagnosticsMock = vi.hoisted(() => ({ items: [] as Array<{path: string; sourceId: string; message: string}> })); +const translateMock = vi.hoisted(() => (key: string) => key); +const diagnosticsMock = vi.hoisted(() => ({ + items: [] as Array<{path: string; sourceId: string; message: string}>, + available: true, +})); const getGlobalSkillSettingsMock = vi.hoisted(() => vi.fn()); const setGlobalSkillDisabledMock = vi.hoisted(() => vi.fn()); const deleteSkillMock = vi.hoisted(() => vi.fn()); @@ -16,16 +20,17 @@ const validateSkillPathMock = vi.hoisted(() => vi.fn()); const notificationMocks = vi.hoisted(() => ({ success: vi.fn(), warning: vi.fn(), + info: vi.fn(), error: vi.fn(), })); vi.mock('@tauri-apps/plugin-dialog', () => ({ open: vi.fn() })); vi.mock('react-i18next', () => ({ - useTranslation: () => ({ t: (key: string) => key }), + useTranslation: () => ({ t: translateMock }), })); vi.mock('@/infrastructure/api', () => ({ configAPI: { - getSkillScanReport: async (...args: unknown[]) => ({ skills: await getSkillConfigsMock(...args), diagnostics: diagnosticsMock.items, diagnosticsAvailable: true }), + getSkillScanReport: async (...args: unknown[]) => ({ skills: await getSkillConfigsMock(...args), diagnostics: diagnosticsMock.items, diagnosticsAvailable: diagnosticsMock.available }), getGlobalSkillSettings: getGlobalSkillSettingsMock, setGlobalSkillDisabled: setGlobalSkillDisabledMock, validateSkillPath: validateSkillPathMock, @@ -79,9 +84,11 @@ describe('useInstalledSkills', () => { validateSkillPathMock.mockReset().mockResolvedValue({ valid: true, name: 'test' }); notificationMocks.success.mockReset(); notificationMocks.warning.mockReset(); + notificationMocks.info.mockReset(); notificationMocks.error.mockReset(); currentInstalled = null; diagnosticsMock.items = []; + diagnosticsMock.available = true; }); afterEach(async () => { @@ -98,6 +105,56 @@ describe('useInstalledSkills', () => { expect(currentInstalled?.skills).toEqual(skills); expect(currentInstalled?.diagnostics).toEqual(diagnosticsMock.items); expect(currentInstalled?.error).toBeNull(); + expect(notificationMocks.warning).toHaveBeenCalledExactlyOnceWith('list.scanIncomplete', { + title: 'nav.title', + metadata: { diagnostics: '/skills/bad/SKILL.md: missing description' }, + }); + }); + + it('does not repeat unchanged scan warnings on refresh, but reports changes and recurrence', async () => { + diagnosticsMock.items = [ + { path: '/skills/first', sourceId: 'codex', message: 'missing target' }, + { path: '/skills/second', sourceId: 'opencode', message: 'permission denied' }, + ]; + await act(async () => root.render()); + expect(notificationMocks.warning).toHaveBeenCalledTimes(1); + + diagnosticsMock.items = [...diagnosticsMock.items].reverse(); + await act(async () => { await currentInstalled?.loadSkills(true); }); + expect(notificationMocks.warning).toHaveBeenCalledTimes(1); + + diagnosticsMock.items = [{ ...diagnosticsMock.items[0], message: 'missing target' }]; + await act(async () => { await currentInstalled?.loadSkills(true); }); + expect(notificationMocks.warning).toHaveBeenCalledTimes(2); + + const failures = diagnosticsMock.items; + diagnosticsMock.items = []; + await act(async () => { await currentInstalled?.loadSkills(true); }); + expect(notificationMocks.warning).toHaveBeenCalledTimes(2); + diagnosticsMock.items = failures; + await act(async () => { await currentInstalled?.loadSkills(true); }); + expect(notificationMocks.warning).toHaveBeenCalledTimes(3); + }); + + it('does not claim skills are available when every scanned skill failed', async () => { + diagnosticsMock.items = [{ path: '/skills/broken', sourceId: 'opencode', message: 'missing target' }]; + await act(async () => root.render()); + expect(currentInstalled?.skills).toEqual([]); + expect(notificationMocks.warning).toHaveBeenCalledExactlyOnceWith('list.loadFailed', { + title: 'nav.title', + metadata: { diagnostics: '/skills/broken: missing target' }, + }); + }); + + it('reports hosts without scan diagnostics once as informational feedback', async () => { + diagnosticsMock.available = false; + await act(async () => root.render()); + await act(async () => { await currentInstalled?.loadSkills(true); }); + expect(currentInstalled?.diagnosticsAvailable).toBe(false); + expect(notificationMocks.warning).not.toHaveBeenCalled(); + expect(notificationMocks.info).toHaveBeenCalledExactlyOnceWith('list.diagnosticsUnavailable', { + title: 'nav.title', + }); }); it('does not query desktop skill configuration during a remote connection' , async () => { @@ -143,7 +200,7 @@ describe('useInstalledSkills', () => { { id: 'source:codex', label: 'Codex' }, ]); expect(currentInstalled?.counts).toEqual({ - all: 7, builtin: 1, suite: 1, user: 1, project: 1, + all: 7, builtin: 1, user: 1, project: 1, 'source:codex': 2, 'source:claude-code': 1, 'source:agent-skills': 1, }); await act(async () => root.render()); @@ -158,6 +215,7 @@ describe('useInstalledSkills', () => { }); it('ignores a desktop skill load that finishes after switching away', async () => { + diagnosticsMock.items = [{ path: '/skills/broken', sourceId: 'opencode', message: 'missing target' }]; let resolveLoad: ((skills: SkillInfo[]) => void) | undefined; getSkillConfigsMock.mockReturnValueOnce(new Promise((resolve) => { resolveLoad = resolve; @@ -188,6 +246,8 @@ describe('useInstalledSkills', () => { expect(currentInstalled?.skills).toEqual([]); expect(container.textContent).toBe('idle'); + expect(notificationMocks.warning).not.toHaveBeenCalled(); + expect(notificationMocks.info).not.toHaveBeenCalled(); }); it('does not notify or reload after a pending delete loses desktop capability', async () => { diff --git a/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts b/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts index d8cfc18f44..c2a5748591 100644 --- a/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts +++ b/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts @@ -1,7 +1,8 @@ -import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'; import { open } from '@tauri-apps/plugin-dialog'; import { useTranslation } from 'react-i18next'; import { configAPI } from '@/infrastructure/api'; +import { getActiveSurfaceScope, onSurfaceActivated } from '@/infrastructure/peer-device/deviceSurface'; import type { SkillInfo, SkillLevel, SkillValidationResult, SkillScanDiagnostic } from '@/infrastructure/config/types'; import { canDeleteSkill, getSkillSourceId, getSkillSourceLabel } from '@/infrastructure/config/skillSourcePresentation'; import { useWorkspaceManagerSync } from '@/infrastructure/hooks/useWorkspaceManagerSync'; @@ -30,8 +31,11 @@ export function useInstalledSkills({ }: UseInstalledSkillsOptions) { const { t } = useTranslation('scenes/skills'); const notification = useNotification(); - const { workspacePath, hasWorkspace, isRemoteWorkspace } = useWorkspaceManagerSync(); + const { warning: notifyScanWarning, info: notifyScanInfo } = notification; + const { workspace, workspacePath, hasWorkspace, isRemoteWorkspace } = useWorkspaceManagerSync(); + const scope = useSyncExternalStore(onSurfaceActivated, getActiveSurfaceScope, getActiveSurfaceScope); + const [loadedContextKey, setLoadedContextKey] = useState(null); const [skills, setSkills] = useState([]); const [diagnostics, setDiagnostics] = useState([]); const [diagnosticsAvailable, setDiagnosticsAvailable] = useState(true); @@ -46,8 +50,9 @@ export function useInstalledSkills({ const [isValidating, setIsValidating] = useState(false); const [isAdding, setIsAdding] = useState(false); const loadRequestIdRef = useRef(0); + const lastScanFeedbackKeyRef = useRef(null); const validationRequestIdRef = useRef(0); - const capabilityKey = `${enabled}\u0000${workspacePath ?? ''}\u0000${isRemoteWorkspace}`; + const capabilityKey = scope.key(scope.epoch, String(enabled), workspace?.id, workspacePath, workspace?.connectionId, String(isRemoteWorkspace)); const capabilityRef = useRef({ key: capabilityKey, epoch: 0, enabled }); useLayoutEffect(() => { if (capabilityRef.current.key !== capabilityKey) { @@ -88,10 +93,35 @@ export function useInstalledSkills({ if (requestId !== loadRequestIdRef.current || !capabilityIsCurrent(capabilityEpoch)) { return; } + setLoadedContextKey(capabilityRef.current.key); setSkills(list.skills); setDiagnostics(list.diagnostics); setDiagnosticsAvailable(list.diagnosticsAvailable); setGloballyDisabledSkillKeys(new Set(globalSettings.globallyDisabledUserSkillKeys)); + + const diagnosticKeys = list.diagnostics + .map(({ sourceId, path, message }) => JSON.stringify([sourceId, path, message])) + .sort(); + const feedbackKey = JSON.stringify([ + capabilityRef.current.key, list.diagnosticsAvailable, diagnosticKeys, + ]); + // Gallery focus and tab re-entry refresh the scan; only changed results notify. + if (lastScanFeedbackKeyRef.current !== feedbackKey) { + lastScanFeedbackKeyRef.current = feedbackKey; + if (list.diagnostics.length > 0) { + const message = list.skills.length > 0 ? t('list.scanIncomplete') : t('list.loadFailed'); + notifyScanWarning(message, { + title: t('nav.title'), + metadata: { + diagnostics: list.diagnostics + .map(({ path, message }) => `${path}: ${message}`) + .join('\n'), + }, + }); + } else if (!list.diagnosticsAvailable) { + notifyScanInfo(t('list.diagnosticsUnavailable'), { title: t('nav.title') }); + } + } } catch (err) { if (requestId !== loadRequestIdRef.current || !capabilityIsCurrent(capabilityEpoch)) { return; @@ -103,7 +133,7 @@ export function useInstalledSkills({ setLoading(false); } } - }, [capabilityIsCurrent, currentCapabilityEpoch, workspacePath]); + }, [capabilityIsCurrent, currentCapabilityEpoch, notifyScanInfo, notifyScanWarning, t, workspacePath]); useEffect(() => { loadRequestIdRef.current += 1; @@ -326,9 +356,7 @@ export function useInstalledSkills({ const filteredSkills = useMemo(() => { return skills.filter((skill) => { let matchesFilter = true; - if (activeFilter === 'suite') { - matchesFilter = skill.isBuiltin; - } else if (activeFilter !== 'all') { + if (activeFilter !== 'all') { matchesFilter = installedSkillGroup(skill) === activeFilter; } @@ -343,7 +371,7 @@ export function useInstalledSkills({ const { counts, sourceGroups } = useMemo(() => { const counts: Record = { - all: skills.length, builtin: 0, user: 0, project: 0, suite: 0, + all: skills.length, builtin: 0, user: 0, project: 0, }; const sources = new Map<`source:${string}`, string>(); for (const skill of skills) { @@ -353,7 +381,6 @@ export function useInstalledSkills({ sources.set(group as `source:${string}`, getSkillSourceLabel(skill, t('list.item.unknownSource'))); } } - counts.suite = counts.builtin; return { counts, sourceGroups: [...sources].sort(([left], [right]) => left.localeCompare(right)) @@ -362,6 +389,8 @@ export function useInstalledSkills({ }, [skills, t]); return { + catalogContextKey: capabilityKey, + catalogReady: enabled && loadedContextKey === capabilityKey && !loading && !error, skills, diagnostics, diagnosticsAvailable, diff --git a/src/web-ui/src/app/scenes/skills/skillsSceneStore.ts b/src/web-ui/src/app/scenes/skills/skillsSceneStore.ts index cd656535f2..a5ef57ad2d 100644 --- a/src/web-ui/src/app/scenes/skills/skillsSceneStore.ts +++ b/src/web-ui/src/app/scenes/skills/skillsSceneStore.ts @@ -1,36 +1,32 @@ import { create } from 'zustand'; -export type InstalledFilter = 'all' | 'builtin' | 'user' | 'project' | 'suite' | `source:${string}`; -export type SuiteModeId = 'agentic' | 'Cowork' | 'Claw'; +export type InstalledFilter = 'all' | 'builtin' | 'user' | 'project' | `source:${string}`; +export type SkillsView = InstalledFilter | 'groups'; interface SkillsSceneState { searchDraft: string; marketQuery: string; - installedFilter: InstalledFilter; + installedView: SkillsView; hideDuplicates: boolean; isAddFormOpen: boolean; - suiteModeId: SuiteModeId; setSearchDraft: (value: string) => void; submitMarketQuery: () => void; - setInstalledFilter: (filter: InstalledFilter) => void; + setInstalledView: (view: SkillsView) => void; setHideDuplicates: (hide: boolean) => void; setAddFormOpen: (open: boolean) => void; toggleAddForm: () => void; - setSuiteModeId: (modeId: SuiteModeId) => void; } export const useSkillsSceneStore = create((set) => ({ searchDraft: '', marketQuery: '', - installedFilter: 'all', + installedView: 'all', hideDuplicates: false, isAddFormOpen: false, - suiteModeId: 'agentic', setSearchDraft: (value) => set({ searchDraft: value }), submitMarketQuery: () => set((state) => ({ marketQuery: state.searchDraft.trim() })), - setInstalledFilter: (filter) => set({ installedFilter: filter }), + setInstalledView: (view) => set({ installedView: view }), setHideDuplicates: (hide) => set({ hideDuplicates: hide }), setAddFormOpen: (open) => set({ isAddFormOpen: open }), toggleAddForm: () => set((state) => ({ isAddFormOpen: !state.isAddFormOpen })), - setSuiteModeId: (modeId) => set({ suiteModeId: modeId }), })); diff --git a/src/web-ui/src/app/scenes/todos/components/TodoEditor.test.tsx b/src/web-ui/src/app/scenes/todos/components/TodoEditor.test.tsx index 2fd003012d..2af7ae8be5 100644 --- a/src/web-ui/src/app/scenes/todos/components/TodoEditor.test.tsx +++ b/src/web-ui/src/app/scenes/todos/components/TodoEditor.test.tsx @@ -20,7 +20,7 @@ vi.mock('@/infrastructure/i18n', () => ({ vi.mock('@/infrastructure/api/service-api/AgentAPI', () => ({ agentAPI: { - getAvailableModes: vi.fn().mockResolvedValue([{ id: 'agentic', name: 'Agentic' }]), + getAvailableModes: vi.fn().mockResolvedValue([{ id: 'Standard', name: 'Agentic' }]), }, })); diff --git a/src/web-ui/src/app/scenes/todos/components/TodoItemRow.test.tsx b/src/web-ui/src/app/scenes/todos/components/TodoItemRow.test.tsx index b0c64824bf..56d86d59a9 100644 --- a/src/web-ui/src/app/scenes/todos/components/TodoItemRow.test.tsx +++ b/src/web-ui/src/app/scenes/todos/components/TodoItemRow.test.tsx @@ -29,7 +29,7 @@ const job: CronJob = { target: { kind: 'workspace', workspace: { workspacePath: '/workspace/openbitfun' }, - launch: { agentType: 'agentic' }, + launch: { agentType: 'Standard' }, }, createdAtMs: 0, configUpdatedAtMs: 0, diff --git a/src/web-ui/src/app/scenes/todos/todoOccurrences.test.ts b/src/web-ui/src/app/scenes/todos/todoOccurrences.test.ts index 1b560a65d1..7534cc6bcb 100644 --- a/src/web-ui/src/app/scenes/todos/todoOccurrences.test.ts +++ b/src/web-ui/src/app/scenes/todos/todoOccurrences.test.ts @@ -35,7 +35,7 @@ function makeJob(overrides: Partial & { schedule: CronSchedule }): Cron target: { kind: 'workspace', workspace: { workspacePath: '/tmp/workspace' }, - launch: { agentType: 'agentic' }, + launch: { agentType: 'Standard' }, }, createdAtMs: 0, configUpdatedAtMs: 0, diff --git a/src/web-ui/src/app/styles/motion.scss b/src/web-ui/src/app/styles/motion.scss index 446ffeb7bf..431c5157b6 100644 --- a/src/web-ui/src/app/styles/motion.scss +++ b/src/web-ui/src/app/styles/motion.scss @@ -4,7 +4,7 @@ * Semantic controls and surfaces participate automatically. Product-specific * motion can opt in without adding another keyframe: * - * data-motion="interactive" press / hover feedback + * data-motion="interactive" color / opacity feedback * data-motion="view" page or panel entrance * data-motion="popup" menu, popover, or floating surface entrance * data-motion="presence" component owns its complete enter/exit lifecycle @@ -76,18 +76,17 @@ @media (prefers-reduced-motion: no-preference) { /** - * Buttons and button-like controls. - * - * Individual transform properties compose with component-owned `transform` - * (icons, drag handles, toggles) instead of replacing it. + * Buttons, button-like controls, and legacy interactive surfaces share paint + * transitions only. Pressing a control must not resize it or its contents. + * Geometry belongs to explicit state changes (toggles, drag, enter/exit), + * never to generic hover/press feedback. In particular, individual `scale` + * would multiply any component-owned `transform: scale(...)`. * * Override contract: this is a zero-specificity `:where()` default, so any * component that declares its own `transition` owns it completely. That is - * deliberate. The property list used to be re-asserted with `!important`, - * which forced all eight properties onto components that had chosen a longer - * duration for one of them — and made `transition: none` unattainable, so an - * element being dragged animated its way to the pointer. Opt out entirely - * with `[data-motion='none']`. + * deliberate: do not re-assert transition properties with `!important` or + * make `transition: none` unattainable for dragging. Opt out entirely with + * `[data-motion='none']`. */ :where( button, @@ -99,15 +98,14 @@ [role='checkbox'], [role='radio'], [role='switch'], - [data-motion='interactive'] + [data-motion='interactive'], + .v-card--interactive, + .openbitfun-context-card--interactive, + .openbitfun-nav-panel__section-header--interactive ):where( :not(:disabled):not([aria-disabled='true']):not([data-motion='none']) ) { - transform-origin: center; transition: - translate var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - scale var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - transform var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), background-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), @@ -115,24 +113,6 @@ opacity var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); } - :where( - button, - input[type='button'], - input[type='submit'], - input[type='reset'], - summary, - [role='button'], - [role='checkbox'], - [role='radio'], - [role='switch'], - [data-motion='interactive'] - ):where( - :not(:disabled):not([aria-disabled='true']):not([data-motion='none']) - ):active { - translate: 0 0; - scale: 0.97; - } - /** * View entrance is explicit. Automatically fading every tab panel makes * ordinary content replacement look like a page refresh, especially when @@ -208,51 +188,6 @@ ):where(:not([data-motion='none'])) { animation: openbitfun-motion-overlay-enter 160ms ease-out both; } - - /** - * A small, explicit set of reusable interactive surfaces that are not native - * buttons. New surfaces should prefer semantic roles or data-motion. - */ - :where( - .v-card--interactive, - .openbitfun-context-card--interactive, - .openbitfun-nav-panel__section-header--interactive - ):where(:not([data-motion='none'])) { - transform-origin: center; - transition: - translate var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - scale var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - transform var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - background-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - box-shadow var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - transition-property: - translate, - scale, - transform, - background-color, - border-color, - box-shadow !important; - } - - @media (hover: hover) and (pointer: fine) { - :where( - .v-card--interactive, - .openbitfun-context-card--interactive, - .openbitfun-nav-panel__section-header--interactive - ):where(:not([data-motion='none'])):hover { - translate: 0 -1px; - } - } - - :where( - .v-card--interactive, - .openbitfun-context-card--interactive, - .openbitfun-nav-panel__section-header--interactive - ):where(:not([data-motion='none'])):active { - translate: 0 0; - scale: 0.99; - } } /* Explicit opt-out for embedded apps, canvases, or latency-sensitive surfaces. */ diff --git a/src/web-ui/src/app/styles/textClipping.test.ts b/src/web-ui/src/app/styles/textClipping.test.ts index cbe3931d7c..c7689d1486 100644 --- a/src/web-ui/src/app/styles/textClipping.test.ts +++ b/src/web-ui/src/app/styles/textClipping.test.ts @@ -85,6 +85,7 @@ describe('Truncated product text line boxes', () => { expect(required).toBeCloseTo(1.172, 3); expect(systemTokens.lineHeight.tight.$value).toBe(1.2); expect(systemTokens.lineHeight.tight.$value).toBeGreaterThanOrEqual(required); + expect(systemTokens.lineHeight.compact.$value).toBeGreaterThanOrEqual(required); expect(systemTokens.lineHeight.base.$value).toBeGreaterThanOrEqual(required); }); @@ -101,8 +102,10 @@ describe('Truncated product text line boxes', () => { ['scenes/skills/SkillsScene.scss', '.skills-card__desc'], ])('%s gives %s a font-relative, descender-safe line height', (filename, selector) => { const lineHeight = selector === '.skills-card__name' - ? 'var(--openbitfun-type-label-md-line-height)' - : 'var(--openbitfun-type-body-sm-line-height)'; + ? 'var(--openbitfun-type-label-sm-line-height)' + : selector === '.skills-card__desc' + ? 'var(--openbitfun-type-meta-line-height)' + : 'var(--openbitfun-type-body-sm-line-height)'; expect(compiledRules(filename)(selector)['line-height']).toBe(lineHeight); }); diff --git a/src/web-ui/src/app/utils/projectSessionWorkspace.test.ts b/src/web-ui/src/app/utils/projectSessionWorkspace.test.ts index 34b99cf45c..eecc0b7bc0 100644 --- a/src/web-ui/src/app/utils/projectSessionWorkspace.test.ts +++ b/src/web-ui/src/app/utils/projectSessionWorkspace.test.ts @@ -26,13 +26,13 @@ const createSession = (overrides: Partial = {}): Session => ({ title: 'Session 1', dialogTurns: [], status: 'idle', - config: { agentType: 'agentic' }, + config: { agentType: 'Standard' }, createdAt: 1, lastActiveAt: 1, error: null, isHistorical: false, maxContextTokens: 128128, - mode: 'agentic', + mode: 'Standard', workspacePath: '/workspace/OpenBitFun', workspaceId: 'workspace-1', sessionKind: 'normal', @@ -58,7 +58,7 @@ describe('findReusableEmptySessionId', () => { activeSessionId: codeSession.sessionId, })); - expect(findReusableEmptySessionId(workspace, 'agentic')).toBeNull(); + expect(findReusableEmptySessionId(workspace, 'Standard')).toBeNull(); }); }); diff --git a/src/web-ui/src/features/dispatch/DispatchJobObserver.test.ts b/src/web-ui/src/features/dispatch/DispatchJobObserver.test.ts index c4df47c5a8..8bc03e580d 100644 --- a/src/web-ui/src/features/dispatch/DispatchJobObserver.test.ts +++ b/src/web-ui/src/features/dispatch/DispatchJobObserver.test.ts @@ -79,7 +79,7 @@ function runningOutboundRecord() { workspacePath: '/repo', promptPreview: 'Dispatch test', title: 'Dispatch test', - agentType: 'agentic', + agentType: 'Standard', approvalPolicy: 'reject-and-report' as const, lastCursor: 0, lastState: 'running' as const, @@ -108,7 +108,7 @@ function registerRunningJob( }, sourceWorkspacePath: '/source', title: 'Dispatch test', - agentType: 'agentic', + agentType: 'Standard', approvalPolicy: 'reject-and-report', cursor: 0, state: 'running', @@ -243,7 +243,7 @@ function installProcessingProjection(): void { }], status: 'idle', config: { - agentType: 'agentic', + agentType: 'Standard', dispatchTargetRequest: { kind: 'ssh', connectionId: 'ssh-1', @@ -264,7 +264,7 @@ function installProcessingProjection(): void { lastActiveAt: 1, error: null, historyState: 'ready', - mode: 'agentic', + mode: 'Standard', workspacePath: '/source', projectWorkspacePath: '/source', sessionKind: 'normal', @@ -893,7 +893,7 @@ describe('DispatchJobObserver', () => { workspacePath: '/target/repo', promptPreview: 'Dispatch test', title: 'Dispatch test', - agentType: 'agentic', + agentType: 'Standard', approvalPolicy: 'reject-and-report' as const, lastCursor: 0, lastState: 'running' as const, @@ -952,7 +952,7 @@ describe('DispatchJobObserver', () => { workspacePath: '/target/repo', promptPreview: 'Dispatch test', title: 'Dispatch test', - agentType: 'agentic', + agentType: 'Standard', approvalPolicy: 'reject-and-report', lastCursor: 0, lastState: 'running', @@ -990,7 +990,7 @@ describe('DispatchJobObserver', () => { expect(context.flowChatStore.addExternalSession).toHaveBeenCalledWith( 'session-restored', 'Dispatch test', - 'agentic', + 'Standard', '/projects/OpenBitFun', expect.objectContaining({ projectWorkspacePath: '/projects/OpenBitFun', diff --git a/src/web-ui/src/features/dispatch/DispatchTargetPicker.scss b/src/web-ui/src/features/dispatch/DispatchTargetPicker.scss index 2be6324a69..63da1154ce 100644 --- a/src/web-ui/src/features/dispatch/DispatchTargetPicker.scss +++ b/src/web-ui/src/features/dispatch/DispatchTargetPicker.scss @@ -47,10 +47,6 @@ opacity: 0.8; } - &:active:not(:disabled) { - transform: scale(0.97); - } - > span:not([data-overflow-content]) { min-width: 0; overflow: hidden; diff --git a/src/web-ui/src/features/dispatch/dispatchApi.test.ts b/src/web-ui/src/features/dispatch/dispatchApi.test.ts index 612a5cafcd..2c5af25da0 100644 --- a/src/web-ui/src/features/dispatch/dispatchApi.test.ts +++ b/src/web-ui/src/features/dispatch/dispatchApi.test.ts @@ -34,7 +34,7 @@ describe('dispatchApi', () => { includeUncommitted: false, jobId: 'job-1', sessionId: 'session-1', - agentType: 'agentic', + agentType: 'Standard', prompt: 'Inspect the repository', approvalPolicy: 'reject-and-report', model: 'target-model', diff --git a/src/web-ui/src/features/dispatch/dispatchJobStore.test.ts b/src/web-ui/src/features/dispatch/dispatchJobStore.test.ts index 6a34cac232..c36dc7ed9b 100644 --- a/src/web-ui/src/features/dispatch/dispatchJobStore.test.ts +++ b/src/web-ui/src/features/dispatch/dispatchJobStore.test.ts @@ -22,7 +22,7 @@ function registerJob(state: 'running' | 'succeeded' = 'running'): void { }, sourceWorkspacePath: '/source', title: 'Dispatch test', - agentType: 'agentic', + agentType: 'Standard', approvalPolicy: 'reject-and-report', cursor: 10, state, @@ -362,7 +362,7 @@ describe('dispatchJobStore', () => { target: record.target, sourceWorkspacePath: '/wrong/current/workspace', title: 'Prompt preview', - agentType: 'agentic', + agentType: 'Standard', approvalPolicy: 'reject-and-report', cursor: 0, state: 'running', diff --git a/src/web-ui/src/features/dispatch/dispatchJobStore.ts b/src/web-ui/src/features/dispatch/dispatchJobStore.ts index 24e2a3fe0e..924d2fa8bb 100644 --- a/src/web-ui/src/features/dispatch/dispatchJobStore.ts +++ b/src/web-ui/src/features/dispatch/dispatchJobStore.ts @@ -451,7 +451,7 @@ export const useDispatchJobStore = create()( sourceWorkspacePath, sourceWorkspaceId: record.sourceWorkspaceId, title: record.title || record.promptPreview || record.sessionId.slice(0, 8), - agentType: record.agentType || 'agentic', + agentType: record.agentType || 'Standard', approvalPolicy: record.approvalPolicy || 'reject-and-report', branch: record.branch, baselineWorktreePath: record.baselineWorktreePath, diff --git a/src/web-ui/src/features/skill-groups/README.md b/src/web-ui/src/features/skill-groups/README.md new file mode 100644 index 0000000000..e39b760224 --- /dev/null +++ b/src/web-ui/src/features/skill-groups/README.md @@ -0,0 +1,35 @@ +# Skill groups + +This feature owns reusable skill group definitions, their resolution against a +skill catalog, and the shared configuration store. Groups keep the existing +version-one `app.user_skill_groups` shape (`id`, `name`, `skillKeys`). + +- The Skills scene creates, edits, copies, reorders, and deletes groups. Built-in + groups come from catalog metadata and are read-only; copying creates a user group. +- The Agent detail picker reads groups and expands selections into the Agent's + final skill keys. Editing or deleting a group never changes Agent configuration, + global skill availability, installed files, or runtime policy. +- Empty groups and unavailable member references remain visible and editable. + An incomplete catalog must not prune references or imply successful discovery. +- The store follows the existing device surface activation scope. It shares loads, + rejects stale writes after a device switch, and serializes mutations in this + window. Each mutation re-reads persisted data and checks the edited group's + original value; unrelated group edits are preserved. This is not a cross-process + compare-and-swap contract. +- Missing legacy configuration defaults to an empty collection. Invalid data or + an unsupported version is an error and must not be rewritten as empty data. + +Catalog availability is scoped to the device and workspace. The Skills scene +keeps its existing explicit unsupported state for Peer Device Mode and non-desktop +hosts. Group definitions are UI preferences, so remote control and detached jobs +continue consuming the existing flattened Agent configuration without a group +resolution dependency on the controller. + +Focused verification: + +```bash +pnpm --dir src/web-ui run test:run src/features/skill-groups/skillGroups.test.ts src/features/skill-groups/skillGroupsStore.test.ts +``` + +These are data and lifecycle contracts. They do not establish visual fidelity or +live SSH, Peer, remote-control, or detached-dispatch behavior. diff --git a/src/web-ui/src/features/skill-groups/skillGroups.test.ts b/src/web-ui/src/features/skill-groups/skillGroups.test.ts new file mode 100644 index 0000000000..2c883b3e7b --- /dev/null +++ b/src/web-ui/src/features/skill-groups/skillGroups.test.ts @@ -0,0 +1,140 @@ +import { describe, expect, it } from 'vitest'; +import type { GroupableSkill } from './skillGroups'; +import { + createUserSkillGroupsConfig, + normalizeUserSkillGroupsConfig, + resolveSkillGroupSummary, + resolveSkillGroups, + setSkillGroupSelection, + resolveSkillSelectionGroups, + saveUserSkillGroup, + deleteUserSkillGroup, + moveUserSkillGroup, +} from './skillGroups'; + +const labels = { + builtin: (groupKey: string) => `builtin:${groupKey}`, + other: 'other', +}; + +const skills: GroupableSkill[] = [ + { key: 'builtin::docs', name: 'Docs', description: '', isBuiltin: true, groupKey: 'office' }, + { key: 'builtin::slides', name: 'Slides', description: '', isBuiltin: true, groupKey: 'office' }, + { key: 'builtin::plan', name: 'Plan', description: '', isBuiltin: true, groupKey: 'planning' }, + { key: 'user::review', name: 'Review', description: '', isBuiltin: false }, +]; + +describe('skillGroups', () => { + it('keeps user groups before backend-owned builtin groups', () => { + const groups = resolveSkillGroups(skills, [{ + id: 'daily', + name: 'Daily work', + skillKeys: ['user::review', 'builtin::docs'], + }], labels); + + expect(groups.map((group) => group.id)).toEqual([ + 'user:daily', + 'builtin:office', + 'builtin:planning', + ]); + expect(groups[0].skills.map((skill) => skill.key)).toEqual(['builtin::docs', 'user::review']); + }); + + it('clears overlapping skills from the final selection', () => { + expect(setSkillGroupSelection( + ['builtin::docs', 'builtin::slides', 'user::review'], + ['builtin::docs', 'user::review'], + false, + )).toEqual(['builtin::slides']); + }); + + it('uses personal groups first when summarizing enabled skills', () => { + const groups = resolveSkillGroupSummary(skills, [{ + id: 'daily', + name: 'Daily work', + skillKeys: ['builtin::docs'], + }], ['builtin::docs', 'builtin::slides'], labels); + + expect(groups.map((group) => group.id)).toEqual(['user:daily', 'builtin:office']); + expect(groups[1].skills.map((skill) => skill.key)).toEqual(['builtin::slides']); + }); + + it('retains unavailable keys while normalizing persisted groups', () => { + const config = normalizeUserSkillGroupsConfig({ + version: 1, + groups: [{ + id: 'daily', + name: 'Daily work', + skillKeys: ['builtin::docs', 'missing::skill', 'builtin::docs'], + }], + }); + + expect(config.groups[0].skillKeys).toEqual(['builtin::docs', 'missing::skill']); + expect(createUserSkillGroupsConfig(config.groups)).toEqual(config); + }); + + it('keeps empty groups and groups whose members are unavailable', () => { + const groups = resolveSkillGroups([], [ + { id: 'empty', name: 'Empty', skillKeys: [] }, + { id: 'offline', name: 'Offline', skillKeys: ['remote::docs'] }, + ], labels); + expect(groups.map(group => group.id)).toEqual(['user:empty', 'user:offline']); + expect(groups[1].unavailableSkillKeys).toEqual(['remote::docs']); + expect(groups[1].skillKeys).toEqual(['remote::docs']); + }); + + it('adds ungrouped skills only to the picker, without duplicating personal members there', () => { + const personal = [{ id: 'daily', name: 'Daily', skillKeys: ['user::review'] }]; + expect(resolveSkillGroups(skills, personal, labels).some(group => group.kind === 'other')).toBe(false); + expect(resolveSkillSelectionGroups(skills, personal, labels).some(group => group.kind === 'other')).toBe(false); + expect(resolveSkillSelectionGroups(skills, [], labels).at(-1)?.skillKeys).toEqual(['user::review']); + }); + + it('deduplicates selections from overlapping groups without changing either definition', () => { + const members = ['builtin::docs', 'user::review']; + const selected = ['builtin::docs', 'builtin::slides']; + expect(setSkillGroupSelection(selected, members, true)).toEqual([ + 'builtin::docs', 'builtin::slides', 'user::review', + ]); + expect(members).toEqual(['builtin::docs', 'user::review']); + expect(selected).toEqual(['builtin::docs', 'builtin::slides']); + }); + + it('opens older installs with no groups and round trips the existing version-one shape', () => { + expect(normalizeUserSkillGroupsConfig(undefined)).toEqual({ version: 1, groups: [] }); + const legacy = { version: 1, groups: [{ id: 'empty', name: 'Empty', skillKeys: [] }] }; + expect(createUserSkillGroupsConfig(normalizeUserSkillGroupsConfig(legacy).groups)).toEqual(legacy); + }); + + it.each([ + 'invalid', { version: 1 }, { version: 2, groups: [] }, + { version: 1, groups: [{ id: 'a', name: 'A', skillKeys: [42] }] }, + { version: 1, groups: [{ id: 'a', name: 'A', skillKeys: [] }, { id: 'a', name: 'B', skillKeys: [] }] }, + ])('refuses unreadable or future data instead of normalizing it into a destructive empty write: %j', value => { + expect(() => normalizeUserSkillGroupsConfig(value)).toThrow(); + }); + + it('merges one edit with the latest collection and preserves unavailable references', () => { + const original = { id: 'daily', name: 'Daily', skillKeys: ['missing::skill'] }; + const elsewhere = { id: 'new', name: 'New', skillKeys: [] }; + const result = saveUserSkillGroup([original, elsewhere], { ...original, name: 'Renamed' }, original); + expect(result).toEqual([{ ...original, name: 'Renamed' }, elsewhere]); + expect(saveUserSkillGroup(result, { id: 'empty', name: 'Empty', skillKeys: [] }, null)).toHaveLength(3); + }); + + it('detects stale edits, stale deletes and duplicate names', () => { + const original = { id: 'daily', name: 'Daily', skillKeys: [] }; + const changed = { ...original, skillKeys: ['new::skill'] }; + expect(() => saveUserSkillGroup([changed], original, original)).toThrow(); + expect(() => deleteUserSkillGroup([changed], original)).toThrow(); + expect(() => saveUserSkillGroup([original], { ...original, id: 'other', name: ' daily ' }, null)).toThrow(); + }); + + it('reorders and removes only group definitions', () => { + const first = { id: 'a', name: 'A', skillKeys: ['user::review'] }; + const second = { id: 'b', name: 'B', skillKeys: ['user::review'] }; + expect(moveUserSkillGroup([first, second], 'b', -1)).toEqual([second, first]); + expect(deleteUserSkillGroup([first, second], first)).toEqual([second]); + expect(second.skillKeys).toEqual(['user::review']); + }); +}); diff --git a/src/web-ui/src/app/scenes/agents/components/skillGroups.ts b/src/web-ui/src/features/skill-groups/skillGroups.ts similarity index 56% rename from src/web-ui/src/app/scenes/agents/components/skillGroups.ts rename to src/web-ui/src/features/skill-groups/skillGroups.ts index 31639e6a38..88f17d1b24 100644 --- a/src/web-ui/src/app/scenes/agents/components/skillGroups.ts +++ b/src/web-ui/src/features/skill-groups/skillGroups.ts @@ -3,6 +3,16 @@ import type { UserSkillGroup, UserSkillGroupsConfig } from '@/infrastructure/con export const USER_SKILL_GROUPS_CONFIG_PATH = 'app.user_skill_groups'; const USER_SKILL_GROUPS_CONFIG_VERSION = 1; +export type SkillGroupErrorCode = 'invalidConfig' | 'unsupportedVersion' | 'nameRequired' + | 'nameDuplicate' | 'conflict' | 'notReady'; + +export class SkillGroupError extends Error { + constructor(readonly code: SkillGroupErrorCode) { + super(`Skill group operation failed: ${code}`); + this.name = 'SkillGroupError'; + } +} + export interface GroupableSkill { key: string; name: string; @@ -23,6 +33,8 @@ export interface ResolvedSkillGroup { kind: ResolvedSkillGroupKind; label: string; skills: GroupableSkill[]; + skillKeys: string[]; + unavailableSkillKeys: string[]; } export interface SkillGroupLabels { @@ -55,23 +67,23 @@ const BUILTIN_SKILL_GROUP_LABEL_KEYS: Record = { }; function normalizeSkillKeys(value: unknown): string[] { - if (!Array.isArray(value)) { - return []; + if (!Array.isArray(value) || value.some(key => typeof key !== 'string' || !key.trim())) { + throw new SkillGroupError('invalidConfig'); } return [...new Set(value.filter((key): key is string => ( typeof key === 'string' && key.trim().length > 0 )).map((key) => key.trim()))]; } -function normalizeUserSkillGroup(value: unknown): UserSkillGroup | null { +function normalizeUserSkillGroup(value: unknown): UserSkillGroup { if (!value || typeof value !== 'object') { - return null; + throw new SkillGroupError('invalidConfig'); } const group = value as Partial; const id = typeof group.id === 'string' ? group.id.trim() : ''; const name = typeof group.name === 'string' ? group.name.trim() : ''; if (!id || !name) { - return null; + throw new SkillGroupError('invalidConfig'); } return { id, name, skillKeys: normalizeSkillKeys(group.skillKeys) }; } @@ -104,18 +116,19 @@ export function builtinSkillGroupLabelKey(groupKey: string): string | null { } export function normalizeUserSkillGroupsConfig(value: unknown): UserSkillGroupsConfig { - if (!value || typeof value !== 'object') { + // Missing on older installs; unreadable or future data must never become an empty write. + if (value === undefined || value === null) { return { version: USER_SKILL_GROUPS_CONFIG_VERSION, groups: [] }; } + if (typeof value !== 'object' || Array.isArray(value)) throw new SkillGroupError('invalidConfig'); const record = value as Partial; - const version = typeof record.version === 'number' && Number.isInteger(record.version) - ? record.version - : USER_SKILL_GROUPS_CONFIG_VERSION; - const groups = Array.isArray(record.groups) - ? record.groups - .map(normalizeUserSkillGroup) - .filter((group): group is UserSkillGroup => group !== null) - : []; + const version = record.version ?? USER_SKILL_GROUPS_CONFIG_VERSION; + if (version !== USER_SKILL_GROUPS_CONFIG_VERSION) throw new SkillGroupError('unsupportedVersion'); + if (!Array.isArray(record.groups)) throw new SkillGroupError('invalidConfig'); + const groups = record.groups.map(normalizeUserSkillGroup); + if (new Set(groups.map(group => group.id)).size !== groups.length) { + throw new SkillGroupError('invalidConfig'); + } return { version, groups }; } @@ -133,13 +146,15 @@ export function resolveSkillGroups( ): ResolvedSkillGroup[] { const availableSkills = activeSkills(skills); const availableByKey = new Map(availableSkills.map((skill) => [skill.key, skill])); - const resolvedUserGroups = userGroups.flatMap((group) => { + const resolvedUserGroups = userGroups.map((group) => { const groupSkills = group.skillKeys .map((key) => availableByKey.get(key)) .filter((skill): skill is GroupableSkill => skill !== undefined); - return groupSkills.length > 0 - ? [{ id: `user:${group.id}`, kind: 'user' as const, label: group.name, skills: sortSkills(groupSkills) }] - : []; + return { + id: `user:${group.id}`, kind: 'user' as const, label: group.name, + skills: sortSkills(groupSkills), skillKeys: group.skillKeys, + unavailableSkillKeys: group.skillKeys.filter(key => !availableByKey.has(key)), + }; }); const builtinByGroup = new Map(); @@ -158,6 +173,8 @@ export function resolveSkillGroups( kind: 'builtin' as const, label: labels.builtin(groupKey), skills: sortSkills(groupSkills), + skillKeys: groupSkills.map(skill => skill.key), + unavailableSkillKeys: [], groupKey, })) .sort((left, right) => ( @@ -165,15 +182,26 @@ export function resolveSkillGroups( || left.label.localeCompare(right.label) )); - const builtinKeys = new Set( - resolvedBuiltinGroups.flatMap((group) => group.skills.map((skill) => skill.key)), - ); - const otherSkills = sortSkills(availableSkills.filter((skill) => !builtinKeys.has(skill.key))); + return [...resolvedUserGroups, ...resolvedBuiltinGroups]; +} + +/** Browsing categories belong only in the picker, never in the managed group collection. */ +export function resolveSkillSelectionGroups( + skills: GroupableSkill[], + userGroups: UserSkillGroup[], + labels: SkillGroupLabels, +): ResolvedSkillGroup[] { + const groups = resolveSkillGroups(skills, userGroups, labels); + const groupedKeys = new Set(groups.flatMap(group => group.skillKeys)); + const otherSkills = sortSkills(activeSkills(skills).filter(skill => !groupedKeys.has(skill.key))); const otherGroup = otherSkills.length > 0 - ? [{ id: 'other', kind: 'other' as const, label: labels.other, skills: otherSkills }] + ? [{ + id: 'other', kind: 'other' as const, label: labels.other, skills: otherSkills, + skillKeys: otherSkills.map(skill => skill.key), unavailableSkillKeys: [], + }] : []; - return [...resolvedUserGroups, ...resolvedBuiltinGroups, ...otherGroup]; + return [...groups, ...otherGroup]; } export function resolveSkillGroupSummary( @@ -184,7 +212,7 @@ export function resolveSkillGroupSummary( ): ResolvedSkillGroup[] { const selected = new Set(selectedSkillKeys); const displayed = new Set(); - return resolveSkillGroups(skills, userGroups, labels).flatMap((group) => { + return resolveSkillSelectionGroups(skills, userGroups, labels).flatMap((group) => { const groupSkills = group.skills.filter((skill) => { if (!selected.has(skill.key) || displayed.has(skill.key)) { return false; @@ -218,10 +246,40 @@ export function toggleSkillSelection(selectedSkillKeys: readonly string[], skill : [...selectedSkillKeys, skillKey]; } -export function unavailableUserSkillKeys( - group: UserSkillGroup, - skills: GroupableSkill[], -): string[] { - const availableKeys = new Set(activeSkills(skills).map((skill) => skill.key)); - return group.skillKeys.filter((key) => !availableKeys.has(key)); +function sameGroup(left: UserSkillGroup | undefined, right: UserSkillGroup): boolean { + return left?.id === right.id && left.name === right.name + && left.skillKeys.length === right.skillKeys.length + && left.skillKeys.every((key, index) => key === right.skillKeys[index]); +} + +/** Apply an edit to the latest collection without overwriting another group's changes. */ +export function saveUserSkillGroup( + groups: UserSkillGroup[], group: UserSkillGroup, original: UserSkillGroup | null, +): UserSkillGroup[] { + const name = group.name.trim(); + if (!name) throw new SkillGroupError('nameRequired'); + const current = groups.find(item => item.id === group.id); + if (original ? !sameGroup(current, original) : current) throw new SkillGroupError('conflict'); + if (groups.some(item => item.id !== group.id && item.name.toLowerCase() === name.toLowerCase())) { + throw new SkillGroupError('nameDuplicate'); + } + const next = normalizeUserSkillGroup({ ...group, name }); + return original ? groups.map(item => item.id === group.id ? next : item) : [...groups, next]; +} + +export function deleteUserSkillGroup(groups: UserSkillGroup[], original: UserSkillGroup): UserSkillGroup[] { + if (!sameGroup(groups.find(group => group.id === original.id), original)) { + throw new SkillGroupError('conflict'); + } + return groups.filter(group => group.id !== original.id); +} + +export function moveUserSkillGroup(groups: UserSkillGroup[], id: string, direction: -1 | 1): UserSkillGroup[] { + const index = groups.findIndex(group => group.id === id); + if (index === -1) throw new SkillGroupError('conflict'); + const nextIndex = index + direction; + if (nextIndex < 0 || nextIndex >= groups.length) return groups; + const next = [...groups]; + [next[index], next[nextIndex]] = [next[nextIndex], next[index]]; + return next; } diff --git a/src/web-ui/src/features/skill-groups/skillGroupsStore.test.ts b/src/web-ui/src/features/skill-groups/skillGroupsStore.test.ts new file mode 100644 index 0000000000..923e3d7bcb --- /dev/null +++ b/src/web-ui/src/features/skill-groups/skillGroupsStore.test.ts @@ -0,0 +1,126 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import type { UserSkillGroupsConfig } from '@/infrastructure/config/types'; +import { activateSurface, getActiveSurfaceScope, resetDeviceSurfaceForTest } from '@/infrastructure/peer-device/deviceSurface'; +import { saveUserSkillGroup } from './skillGroups'; +import { createSkillGroupsStore } from './skillGroupsStore'; + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise(resolvePromise => { resolve = resolvePromise; }); + return { promise, resolve }; +} + +const initial: UserSkillGroupsConfig = { + version: 1, groups: [{ id: 'daily', name: 'Daily', skillKeys: ['unavailable::skill'] }], +}; + +describe('shared skill group persistence', () => { + beforeEach(() => resetDeviceSurfaceForTest()); + + it('shares one load and merges edits with fresh persisted data', async () => { + let saved = structuredClone(initial); + let reads = 0; + let writes = 0; + const store = createSkillGroupsStore({ + read: async () => { reads += 1; return saved; }, + write: async config => { writes += 1; saved = config; }, + }); + const scope = getActiveSurfaceScope(); + await Promise.all([store.getState().load(scope), store.getState().load(scope)]); + expect(reads).toBe(1); + const original = store.getState().groups[0]; + saved = { ...saved, groups: [...saved.groups, { id: 'new', name: 'New elsewhere', skillKeys: [] }] }; + await store.getState().update(scope, groups => saveUserSkillGroup(groups, { ...original, name: 'Renamed' }, original)); + expect(writes).toBe(1); + expect(saved.groups).toEqual([{ ...original, name: 'Renamed' }, { id: 'new', name: 'New elsewhere', skillKeys: [] }]); + expect(store.getState().groups).toEqual(saved.groups); + }); + + it('does not permit a write after a failed load', async () => { + let writes = 0; + const store = createSkillGroupsStore({ + read: async () => { throw new Error('Host unavailable'); }, + write: async () => { writes += 1; }, + }); + const scope = getActiveSurfaceScope(); + await store.getState().load(scope); + expect(store.getState().status).toBe('error'); + await expect(store.getState().update(scope, () => [])).rejects.toThrow(); + expect(writes).toBe(0); + }); + + it('refuses to overwrite data that changed to a newer format after loading', async () => { + let saved: unknown = initial; + let writes = 0; + const store = createSkillGroupsStore({ read: async () => saved, write: async () => { writes += 1; } }); + const scope = getActiveSurfaceScope(); + await store.getState().load(scope); + saved = { version: 2, groups: [] }; + await expect(store.getState().update(scope, () => [])).rejects.toThrow(); + expect(writes).toBe(0); + expect(store.getState().groups).toEqual(initial.groups); + expect(store.getState().saving).toBe(false); + }); + + it('keeps committed groups after a failed save and allows retry', async () => { + let fail = true; + const store = createSkillGroupsStore({ + read: async () => initial, + write: async () => { if (fail) throw new Error('Write failed'); }, + }); + const scope = getActiveSurfaceScope(); + await store.getState().load(scope); + await expect(store.getState().update(scope, () => [])).rejects.toThrow('Write failed'); + expect(store.getState().groups).toEqual(initial.groups); + expect(store.getState().saving).toBe(false); + fail = false; + await store.getState().update(scope, () => []); + expect(store.getState().groups).toEqual([]); + }); + + it('rejects a second mutation while a save is pending', async () => { + const pending = deferred(); + const store = createSkillGroupsStore({ read: async () => initial, write: () => pending.promise }); + const scope = getActiveSurfaceScope(); + await store.getState().load(scope); + const saving = store.getState().update(scope, groups => groups); + await expect(store.getState().update(scope, () => [])).rejects.toThrow(); + pending.resolve(); + await saving; + expect(store.getState().groups).toEqual(initial.groups); + }); + + it('drops responses from a previous device activation', async () => { + const oldRead = deferred(); + let reads = 0; + const store = createSkillGroupsStore({ + read: () => ++reads === 1 ? oldRead.promise : Promise.resolve({ version: 1, groups: [] }), + write: async () => {}, + }); + const oldLoad = store.getState().load(getActiveSurfaceScope()); + const next = activateSurface('peer-device'); + await store.getState().load(next); + oldRead.resolve(initial); + await oldLoad; + expect(store.getState().epoch).toBe(next.epoch); + expect(store.getState().groups).toEqual([]); + }); + + it('does not submit an old draft through another device transport', async () => { + const readBeforeWrite = deferred(); + let reads = 0; + let writes = 0; + const store = createSkillGroupsStore({ + read: () => ++reads === 2 ? readBeforeWrite.promise : Promise.resolve(initial), + write: async () => { writes += 1; }, + }); + const scope = getActiveSurfaceScope(); + await store.getState().load(scope); + const saving = store.getState().update(scope, () => []); + const rejected = expect(saving).rejects.toMatchObject({ isSurfaceChangedError: true }); + activateSurface('peer-device'); + readBeforeWrite.resolve(initial); + await rejected; + expect(writes).toBe(0); + }); +}); diff --git a/src/web-ui/src/features/skill-groups/skillGroupsStore.ts b/src/web-ui/src/features/skill-groups/skillGroupsStore.ts new file mode 100644 index 0000000000..8a520b8b06 --- /dev/null +++ b/src/web-ui/src/features/skill-groups/skillGroupsStore.ts @@ -0,0 +1,71 @@ +import { createStore } from 'zustand/vanilla'; +import type { UserSkillGroup, UserSkillGroupsConfig } from '@/infrastructure/config/types'; +import type { SurfaceScope } from '@/infrastructure/peer-device/deviceSurface'; +import { createUserSkillGroupsConfig, normalizeUserSkillGroupsConfig, SkillGroupError } from './skillGroups'; + +interface SkillGroupsRepository { + read: () => Promise; + write: (config: UserSkillGroupsConfig) => Promise; +} + +interface SkillGroupsState { + epoch: number; + groups: UserSkillGroup[]; + status: 'idle' | 'loading' | 'ready' | 'error'; + error: unknown; + saving: boolean; + load: (scope: SurfaceScope, force?: boolean) => Promise; + update: (scope: SurfaceScope, change: (groups: UserSkillGroup[]) => UserSkillGroup[]) => Promise; +} + +/** One collection per active host, shared by management and agent pickers. */ +export function createSkillGroupsStore(repository: SkillGroupsRepository) { + let requestId = 0; + let pendingLoad: Promise | undefined; + return createStore((set, get) => ({ + epoch: 0, groups: [], status: 'idle', error: null, saving: false, + load: (scope, force = false) => { + if (!scope.isCurrent()) return Promise.resolve(); + const current = get(); + if (current.epoch === scope.epoch) { + if (current.saving || (!force && current.status === 'ready')) return Promise.resolve(); + if (current.status === 'loading' && pendingLoad) return pendingLoad; + } + const request = ++requestId; + set({ + epoch: scope.epoch, groups: current.epoch === scope.epoch ? current.groups : [], + status: 'loading', error: null, saving: false, + }); + pendingLoad = (async () => { + try { + const config = normalizeUserSkillGroupsConfig(await repository.read()); + if (scope.isCurrent() && request === requestId) { + set({ groups: config.groups, status: 'ready', error: null }); + } + } catch (error) { + if (scope.isCurrent() && request === requestId) set({ status: 'error', error }); + } + })(); + return pendingLoad; + }, + update: async (scope, change) => { + scope.assertCurrent('update skill groups'); + const state = get(); + if (state.epoch !== scope.epoch || state.status !== 'ready' || state.saving) { + throw new SkillGroupError('notReady'); + } + set({ saving: true }); + try { + // Re-read before a mutation so a stale page does not overwrite unrelated edits. + const latest = normalizeUserSkillGroupsConfig(await repository.read()); + scope.assertCurrent('update skill groups'); + const next = createUserSkillGroupsConfig(change(latest.groups)); + await repository.write(next); + scope.assertCurrent('update skill groups'); + set({ groups: next.groups, error: null }); + } finally { + if (scope.isCurrent()) set({ saving: false }); + } + }, + })); +} diff --git a/src/web-ui/src/features/skill-groups/useUserSkillGroups.ts b/src/web-ui/src/features/skill-groups/useUserSkillGroups.ts new file mode 100644 index 0000000000..36a543f378 --- /dev/null +++ b/src/web-ui/src/features/skill-groups/useUserSkillGroups.ts @@ -0,0 +1,37 @@ +import { useCallback, useEffect, useSyncExternalStore } from 'react'; +import { useStore } from 'zustand'; +import { configAPI } from '@/infrastructure/api/service-api/ConfigAPI'; +import type { UserSkillGroup } from '@/infrastructure/config/types'; +import { getActiveSurfaceScope, onSurfaceActivated } from '@/infrastructure/peer-device/deviceSurface'; +import { USER_SKILL_GROUPS_CONFIG_PATH } from './skillGroups'; +import { createSkillGroupsStore } from './skillGroupsStore'; + +const store = createSkillGroupsStore({ + read: () => configAPI.getConfig(USER_SKILL_GROUPS_CONFIG_PATH), + write: config => configAPI.setConfig(USER_SKILL_GROUPS_CONFIG_PATH, config), +}); +const EMPTY_GROUPS: UserSkillGroup[] = []; + +export function useUserSkillGroups(enabled = true) { + const scope = useSyncExternalStore(onSurfaceActivated, getActiveSurfaceScope, getActiveSurfaceScope); + const state = useStore(store); + const { load, update } = state; + useEffect(() => { + if (enabled) void load(scope); + }, [enabled, load, scope]); + const reload = useCallback(() => enabled ? load(scope, true) : Promise.resolve(), [enabled, load, scope]); + const updateGroups = useCallback((change: (groups: UserSkillGroup[]) => UserSkillGroup[]) => { + if (!enabled) return Promise.reject(new Error('Skill group management is unavailable')); + return update(scope, change); + }, [enabled, scope, update]); + const current = enabled && state.epoch === scope.epoch; + return { + scope, + groups: current ? state.groups : EMPTY_GROUPS, + loading: enabled && (!current || state.status === 'idle' || state.status === 'loading'), + error: current ? state.error : null, + ready: current && state.status === 'ready', + saving: current && state.saving, + reload, updateGroups, + }; +} diff --git a/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.scss b/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.scss index 7429e834e2..776c29b757 100644 --- a/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.scss +++ b/src/web-ui/src/features/ssh-remote/SSHConnectionDialog.scss @@ -84,10 +84,6 @@ background: var(--openbitfun-color-action-quiet-hover); } - &:active { - transform: scale(0.99); - } - &--config { .ssh-connection-dialog__saved-icon { background: var(--openbitfun-color-status-success-surface); diff --git a/src/web-ui/src/flow_chat/components/ChatInput.scss b/src/web-ui/src/flow_chat/components/ChatInput.scss index d52f794435..c00cede386 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.scss +++ b/src/web-ui/src/flow_chat/components/ChatInput.scss @@ -266,10 +266,6 @@ ):not(:disabled):hover { color: var(--openbitfun-color-content-primary); } - - &:active:not(:disabled) { - transform: scale(0.97); - } } @keyframes openbitfun-voice-input-spin { to { transform: rotate(360deg); } } @@ -691,10 +687,6 @@ color: var(--openbitfun-color-content-primary); } - &:active:not(:disabled) { - transform: scale(0.97); - } - &:hover:not(#{&}--active) { color: var(--openbitfun-color-content-secondary); background: color-mix(in srgb, var(--openbitfun-color-content-on-dark) 4%, transparent); @@ -838,10 +830,6 @@ opacity: 1; } - &:active { - transform: scale(0.97); - } - &:focus-visible { opacity: 1; outline: 2px solid color-mix(in srgb, var(--openbitfun-color-accent-hover) 40%, transparent); @@ -1003,10 +991,6 @@ background: var(--openbitfun-color-action-neutral-surface-hover); } - &:active { - transform: scale(0.99); - } - &--active { .openbitfun-chat-input__slash-command-name { color: color-mix(in srgb, var(--openbitfun-color-accent-default) 95%, var(--openbitfun-color-content-primary)); diff --git a/src/web-ui/src/flow_chat/components/ChatInputApprovalBand.test.tsx b/src/web-ui/src/flow_chat/components/ChatInputApprovalBand.test.tsx index dffda09b76..b8405798ca 100644 --- a/src/web-ui/src/flow_chat/components/ChatInputApprovalBand.test.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInputApprovalBand.test.tsx @@ -63,7 +63,7 @@ function request(overrides: Partial = {}): PermissionRequest toolCallId: 'tool-1', projectPath: '/workspace/OpenBitFun', projectId: 'project-1', - agentId: 'agentic', + agentId: 'Standard', action: 'edit', resources: ['src/main.rs'], saveResources: ['src/main.rs'], diff --git a/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStripLayout.test.ts b/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStripLayout.test.ts index dd7a9aed2c..57587b10a9 100644 --- a/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStripLayout.test.ts +++ b/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStripLayout.test.ts @@ -1,6 +1,7 @@ import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; +import { HARNESS_PRESENTATION } from '@/shared/agents/harnessPresentation'; function readLocalFile(name: string): string { return readFileSync(fileURLToPath(new URL(`./${name}`, import.meta.url)), 'utf8') @@ -329,7 +330,7 @@ describe('composer context track layout', () => { 'composer.setValue(newSessionId, transferredDraft.value)', ); expect(chatInput).not.toContain('data-testid="chat-input-agent-mode-chip"'); - expect(chatInput).not.toContain("modeState.current !== 'agentic'"); + expect(chatInput).not.toContain("modeState.current !== 'Standard'"); expect(chatInput).toContain('!isMultiLine && executionLevelPolicy.userConfigurable ? ('); }); @@ -502,11 +503,16 @@ describe('composer context track layout', () => { const component = readLocalFile('HarnessProfileSelector.tsx'); const stylesheet = readLocalFile('HarnessProfileSelector.scss'); - expect(component).toMatch(/minimal: 'minimal',[\s\S]*?balanced: 'standard',[\s\S]*?ultimate: 'ultimate',[\s\S]*?creative: 'creative',/); + expect(HARNESS_PRESENTATION).toEqual({ + Minimal: { icon: 'minimal', gear: 1 }, + Standard: { icon: 'standard', gear: 2 }, + Ultimate: { icon: 'ultimate', gear: 3 }, + Creative: { icon: 'creative', gear: 'creative' }, + }); expect(component).toContain( - 'data-harness-density={densityProfile ? PROFILE_GEARS[densityProfile] : 0}', + 'data-harness-density={densityProfile ? HARNESS_PRESENTATION[densityProfile].gear : 0}', ); - expect(component).toContain('name={PROFILE_ICONS[profile]}'); + expect(component).toContain('name={HARNESS_PRESENTATION[profile].icon}'); expect(component).not.toContain('className="openbitfun-harness-selector__density-core"'); expect(component).toContain(''); expect(component).toContain(''); diff --git a/src/web-ui/src/flow_chat/components/FlowTextBlock.scss b/src/web-ui/src/flow_chat/components/FlowTextBlock.scss index 90924c1b39..600f4f3519 100644 --- a/src/web-ui/src/flow_chat/components/FlowTextBlock.scss +++ b/src/web-ui/src/flow_chat/components/FlowTextBlock.scss @@ -97,25 +97,21 @@ height: 26px; padding: 0.2rem; transition: - transform 120ms cubic-bezier(0.23, 1, 0.32, 1), opacity 140ms ease; } .markdown-renderer .code-block-wrapper .copy-button:hover { background: transparent; color: var(--openbitfun-color-content-muted); - transform: scale(1.08); } .markdown-renderer .code-block-wrapper .copy-button:active { background: transparent; color: var(--openbitfun-color-content-muted); - transform: scale(1.02); } .markdown-renderer .code-block-wrapper .copy-button.copy-success:hover { color: var(--openbitfun-color-status-success-content); - transform: scale(1.08); } .text-content { diff --git a/src/web-ui/src/flow_chat/components/HarnessProfileSelector.appearance.ts b/src/web-ui/src/flow_chat/components/HarnessProfileSelector.appearance.ts index 51d6471c86..1e7bc56e9f 100644 --- a/src/web-ui/src/flow_chat/components/HarnessProfileSelector.appearance.ts +++ b/src/web-ui/src/flow_chat/components/HarnessProfileSelector.appearance.ts @@ -18,7 +18,7 @@ export const harnessProfileSelectorAppearanceDescriptor: AppearanceSurfaceDescri { id: 'profile', attribute: 'data-openbitfun-profile', - values: ['minimal', 'balanced', 'ultimate', 'creative', 'other'], + values: ['Minimal', 'Standard', 'Ultimate', 'Creative', 'other'], }, ], states: [ diff --git a/src/web-ui/src/flow_chat/components/HarnessProfileSelector.scss b/src/web-ui/src/flow_chat/components/HarnessProfileSelector.scss index aca4e5dfb6..496f8d1fc9 100644 --- a/src/web-ui/src/flow_chat/components/HarnessProfileSelector.scss +++ b/src/web-ui/src/flow_chat/components/HarnessProfileSelector.scss @@ -65,19 +65,19 @@ width: 100%; - &[data-openbitfun-profile='minimal'] { + &[data-openbitfun-profile='Minimal'] { --openbitfun-harness-profile-accent: var(--openbitfun-color-identity-harness-minimal); } - &[data-openbitfun-profile='balanced'] { + &[data-openbitfun-profile='Standard'] { --openbitfun-harness-profile-accent: var(--openbitfun-color-identity-harness-standard); } - &[data-openbitfun-profile='ultimate'] { + &[data-openbitfun-profile='Ultimate'] { --openbitfun-harness-profile-accent: var(--openbitfun-color-identity-harness-ultimate); } - &[data-openbitfun-profile='creative'] { + &[data-openbitfun-profile='Creative'] { --openbitfun-harness-profile-accent: var(--openbitfun-color-identity-harness-creative); } @@ -87,14 +87,14 @@ font-weight: var(--openbitfun-type-label-sm-font-weight); } - &[data-openbitfun-profile='ultimate'] [data-openbitfun-component='action-item'] { + &[data-openbitfun-profile='Ultimate'] [data-openbitfun-component='action-item'] { [data-openbitfun-part='label'] { font-style: italic; font-weight: var(--openbitfun-type-label-selected-font-weight); } } - &[data-openbitfun-profile='creative'] [data-openbitfun-component='action-item'] { + &[data-openbitfun-profile='Creative'] [data-openbitfun-component='action-item'] { [data-openbitfun-part='label'] { font-family: var(--openbitfun-type-flow-code-font-family); font-weight: var(--openbitfun-type-label-sm-font-weight); @@ -108,10 +108,10 @@ // the glow keeps the same geometry for every Harness identity color. .openbitfun-chat-input__mode-dropdown--agent-boost:has( .openbitfun-harness-selector--menu-item:is( - [data-openbitfun-profile='minimal'], - [data-openbitfun-profile='balanced'], - [data-openbitfun-profile='ultimate'], - [data-openbitfun-profile='creative'] + [data-openbitfun-profile='Minimal'], + [data-openbitfun-profile='Standard'], + [data-openbitfun-profile='Ultimate'], + [data-openbitfun-profile='Creative'] ) ) { overflow: hidden; @@ -143,25 +143,25 @@ } .openbitfun-chat-input__mode-dropdown--agent-boost:has( - .openbitfun-harness-selector--menu-item[data-openbitfun-profile='minimal'] + .openbitfun-harness-selector--menu-item[data-openbitfun-profile='Minimal'] ) { --openbitfun-harness-profile-accent: var(--openbitfun-color-identity-harness-minimal); } .openbitfun-chat-input__mode-dropdown--agent-boost:has( - .openbitfun-harness-selector--menu-item[data-openbitfun-profile='balanced'] + .openbitfun-harness-selector--menu-item[data-openbitfun-profile='Standard'] ) { --openbitfun-harness-profile-accent: var(--openbitfun-color-identity-harness-standard); } .openbitfun-chat-input__mode-dropdown--agent-boost:has( - .openbitfun-harness-selector--menu-item[data-openbitfun-profile='ultimate'] + .openbitfun-harness-selector--menu-item[data-openbitfun-profile='Ultimate'] ) { --openbitfun-harness-profile-accent: var(--openbitfun-color-identity-harness-ultimate); } .openbitfun-chat-input__mode-dropdown--agent-boost:has( - .openbitfun-harness-selector--menu-item[data-openbitfun-profile='creative'] + .openbitfun-harness-selector--menu-item[data-openbitfun-profile='Creative'] ) { --openbitfun-harness-profile-accent: var(--openbitfun-color-identity-harness-creative); } @@ -184,19 +184,19 @@ color: currentColor; } -.openbitfun-harness-selector__density-mark[data-harness-profile='minimal'] { +.openbitfun-harness-selector__density-mark[data-harness-profile='Minimal'] { color: var(--openbitfun-color-identity-harness-minimal); } -.openbitfun-harness-selector__density-mark[data-harness-profile='balanced'] { +.openbitfun-harness-selector__density-mark[data-harness-profile='Standard'] { color: var(--openbitfun-color-identity-harness-standard); } -.openbitfun-harness-selector__density-mark[data-harness-profile='ultimate'] { +.openbitfun-harness-selector__density-mark[data-harness-profile='Ultimate'] { color: var(--openbitfun-color-identity-harness-ultimate); } -.openbitfun-harness-selector__density-mark[data-harness-profile='creative'] { +.openbitfun-harness-selector__density-mark[data-harness-profile='Creative'] { color: var(--openbitfun-color-identity-harness-creative); } diff --git a/src/web-ui/src/flow_chat/components/HarnessProfileSelector.test.tsx b/src/web-ui/src/flow_chat/components/HarnessProfileSelector.test.tsx index bbb74eced0..d2f54ded7f 100644 --- a/src/web-ui/src/flow_chat/components/HarnessProfileSelector.test.tsx +++ b/src/web-ui/src/flow_chat/components/HarnessProfileSelector.test.tsx @@ -66,7 +66,7 @@ describe('HarnessProfileSelector', () => { await act(async () => { root.render( , ); @@ -76,7 +76,7 @@ describe('HarnessProfileSelector', () => { '[data-testid="harness-profile-selector"]', ); expect(trigger?.querySelector('.openbitfun-harness-selector__density-mark')).toBeNull(); - expect(trigger?.textContent).toBe('chatInput.harness.profiles.balanced.name'); + expect(trigger?.textContent).toBe('chatInput.harness.profiles.Standard.name'); expect(trigger?.dataset.harnessPending).toBeUndefined(); expect( container.querySelector('[data-testid="harness-profile-pending-dot"]'), @@ -85,7 +85,7 @@ describe('HarnessProfileSelector', () => { it('renders the authoritative selected profile without a pending projection', async () => { await act(async () => { - root.render(); + root.render(); }); const trigger = container.querySelector( @@ -117,7 +117,7 @@ describe('HarnessProfileSelector', () => { root.render( , ); @@ -137,7 +137,7 @@ describe('HarnessProfileSelector', () => {
    { '[data-testid="harness-profile-selector"]', ); expect(selectorRoot?.dataset.openbitfunPresentation).toBe('menu-item'); - expect(selectorRoot?.dataset.openbitfunProfile).toBe('balanced'); + expect(selectorRoot?.dataset.openbitfunProfile).toBe('Standard'); expect(trigger?.querySelector('.openbitfun-harness-selector__trigger-chevron')).not.toBeNull(); const triggerMark = trigger?.querySelector( '.openbitfun-harness-selector__density-mark', ); - expect(triggerMark?.dataset.harnessProfile).toBe('balanced'); + expect(triggerMark?.dataset.harnessProfile).toBe('Standard'); expect(triggerMark?.dataset.harnessDensity).toBe('2'); expect(triggerMark?.querySelector('[data-openbitfun-name="standard"][data-size="md"]')).not.toBeNull(); expect( trigger?.querySelector('[data-openbitfun-part="label"]')?.textContent, - ).toBe('chatInput.harness.profiles.balanced.name'); + ).toBe('chatInput.harness.profiles.Standard.name'); expect(trigger?.textContent).not.toContain('chatInput.harness.menuLabel'); expect(trigger?.textContent).not.toContain('chatInput.current'); expect(trigger?.getAttribute('aria-controls')).toBeTruthy(); @@ -214,7 +214,7 @@ describe('HarnessProfileSelector', () => { root.render( , ); @@ -242,7 +242,7 @@ describe('HarnessProfileSelector', () => { await act(async () => { root.render( { expect(menu).not.toBeNull(); const rows = Array.from(menu!.querySelectorAll('[data-openbitfun-part="profile"]')); expect(rows.map(row => row.dataset.openbitfunProfile)).toEqual([ - 'minimal', - 'balanced', - 'ultimate', - 'creative', + 'Minimal', + 'Standard', + 'Ultimate', + 'Creative', 'other', ]); expect(rows.map(row => density(row))).toEqual([1, 2, 3, 0, 0]); @@ -289,7 +289,7 @@ describe('HarnessProfileSelector', () => { await act(async () => { root.render( { it('activates every implemented profile including Creative', async () => { const onSelectProfile = vi.fn(); await act(async () => { - root.render(); + root.render(); }); await act(async () => { container.querySelector('[data-testid="harness-profile-selector"]') ?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); await act(async () => { - document.querySelector('[data-testid="harness-profile-ultimate"]') + document.querySelector('[data-testid="harness-profile-Ultimate"]') ?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); - expect(onSelectProfile).toHaveBeenCalledWith('ultimate'); + expect(onSelectProfile).toHaveBeenCalledWith('Ultimate'); expect(onSelectProfile).toHaveBeenCalledTimes(1); await act(async () => { @@ -359,24 +359,24 @@ describe('HarnessProfileSelector', () => { ?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); await act(async () => { - document.querySelector('[data-testid="harness-profile-creative"]') + document.querySelector('[data-testid="harness-profile-Creative"]') ?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); expect(notify.info).not.toHaveBeenCalled(); expect(onSelectProfile).toHaveBeenCalledTimes(2); - expect(onSelectProfile).toHaveBeenLastCalledWith('creative'); + expect(onSelectProfile).toHaveBeenLastCalledWith('Creative'); await act(async () => { container.querySelector('[data-testid="harness-profile-selector"]') ?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); await act(async () => { - document.querySelector('[data-testid="harness-profile-minimal"]') + document.querySelector('[data-testid="harness-profile-Minimal"]') ?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); expect(onSelectProfile).toHaveBeenCalledTimes(3); - expect(onSelectProfile).toHaveBeenLastCalledWith('minimal'); - expect(onSelectProfile).toHaveBeenCalledWith('minimal'); + expect(onSelectProfile).toHaveBeenLastCalledWith('Minimal'); + expect(onSelectProfile).toHaveBeenCalledWith('Minimal'); expect(document.querySelector('.openbitfun-harness-selector__menu')).toBeNull(); }); @@ -387,7 +387,7 @@ describe('HarnessProfileSelector', () => { root.render( { expect(trigger?.dataset.harnessLocked).toBe('true'); expect(trigger?.dataset.harnessFixed).toBe('true'); expect(trigger?.disabled).toBe(false); - expect(trigger?.textContent).toBe('chatInput.harness.profiles.balanced.name'); + expect(trigger?.textContent).toBe('chatInput.harness.profiles.Standard.name'); await act(async () => { trigger?.dispatchEvent(new MouseEvent('click', { bubbles: true })); @@ -413,16 +413,16 @@ describe('HarnessProfileSelector', () => { expect(menu?.querySelector('[data-testid="harness-session-summary"]')).toBeNull(); expect(menu?.querySelector('[data-testid="harness-start-new-session"]')).toBeNull(); expect( - menu?.querySelector('[data-testid="harness-profile-minimal"]') + menu?.querySelector('[data-testid="harness-profile-Minimal"]') ?.getAttribute('role'), ).toBe('menuitem'); expect( - menu?.querySelector('[data-testid="harness-profile-minimal"]') + menu?.querySelector('[data-testid="harness-profile-Minimal"]') ?.dataset.openbitfunState, ).toBe('available'); await act(async () => { - menu?.querySelector('[data-testid="harness-profile-minimal"]') + menu?.querySelector('[data-testid="harness-profile-Minimal"]') ?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); expect(onSelectProfile).not.toHaveBeenCalled(); @@ -432,7 +432,7 @@ describe('HarnessProfileSelector', () => { confirmText: 'chatInput.harness.newSessionConfirmation.confirm', }); expect(onStartNewSession).toHaveBeenCalledWith( - { kind: 'profile', id: 'minimal' }, + { kind: 'profile', id: 'Minimal' }, ); expect(document.querySelector('.openbitfun-harness-selector__menu')).toBeNull(); }); @@ -495,7 +495,7 @@ describe('HarnessProfileSelector', () => { root.render( , @@ -507,7 +507,7 @@ describe('HarnessProfileSelector', () => { ?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); await act(async () => { - document.querySelector('[data-testid="harness-profile-ultimate"]') + document.querySelector('[data-testid="harness-profile-Ultimate"]') ?.dispatchEvent(new MouseEvent('click', { bubbles: true })); await Promise.resolve(); }); @@ -517,7 +517,7 @@ describe('HarnessProfileSelector', () => { expect(document.querySelector('.openbitfun-harness-selector__menu')).toBeNull(); }); - it.each(['creative'] as const)( + it.each(['Creative'] as const)( 'presents a persisted %s profile as active', async (profileId) => { await act(async () => { @@ -543,7 +543,7 @@ describe('HarnessProfileSelector', () => { root.render( , @@ -560,12 +560,12 @@ describe('HarnessProfileSelector', () => { trigger?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); await act(async () => { - document.querySelector('[data-testid="harness-profile-balanced"]') + document.querySelector('[data-testid="harness-profile-Standard"]') ?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); expect(onSelectProfile).not.toHaveBeenCalled(); expect(onStartNewSession).toHaveBeenCalledWith( - { kind: 'profile', id: 'balanced' }, + { kind: 'profile', id: 'Standard' }, ); expect(notify.info).not.toHaveBeenCalled(); }); diff --git a/src/web-ui/src/flow_chat/components/HarnessProfileSelector.tsx b/src/web-ui/src/flow_chat/components/HarnessProfileSelector.tsx index 2d27eeeb3d..aa1f7bb175 100644 --- a/src/web-ui/src/flow_chat/components/HarnessProfileSelector.tsx +++ b/src/web-ui/src/flow_chat/components/HarnessProfileSelector.tsx @@ -1,7 +1,9 @@ +import { HARNESS_IDS, canonicalAgentId, type HarnessId } from '@/shared/agents/identity'; +import { HARNESS_PRESENTATION } from '@/shared/agents/harnessPresentation'; import React, { useCallback, useEffect, useId, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; -import { OverflowText, Icon, Menu, MenuItem, MenuSection, MenuSeparator, Tooltip, type IconName } from '@openbitfun/ui'; +import { OverflowText, Icon, Menu, MenuItem, MenuSection, MenuSeparator, Tooltip } from '@openbitfun/ui'; import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost'; import { confirmDialog } from '@/infrastructure/confirm-dialog'; import { notificationService } from '@/shared/notification-system'; @@ -10,13 +12,9 @@ import { useSideAnchoredPopoverPosition } from '@/shared/utils/useSideAnchoredPo import './HarnessProfileSelector.scss'; export type HarnessProfileId = KnownHarnessProfileId | (string & {}); -export type KnownHarnessProfileId = - | 'minimal' - | 'balanced' - | 'ultimate' - | 'creative' - | 'other'; -export type SelectableHarnessProfileId = 'minimal' | 'balanced' | 'ultimate' | 'creative'; +/** Includes the existing Agent submenu, which is navigation rather than an identity. */ +export type KnownHarnessProfileId = HarnessId | 'other'; +export type SelectableHarnessProfileId = HarnessId; export interface HarnessAgentOption { id: string; @@ -57,40 +55,21 @@ interface HarnessProfileSelectorProps { onSelectionComplete?: () => void; } -const PROFILE_IDS: KnownHarnessProfileId[] = [ - 'minimal', - 'balanced', - 'ultimate', - 'creative', - 'other', -]; -type DensityHarnessProfileId = 'minimal' | 'balanced' | 'ultimate'; - -const PROFILE_GEARS: Record = { - minimal: 1, - balanced: 2, - ultimate: 3, -}; - -const PROFILE_ICONS: Record = { - minimal: 'minimal', - balanced: 'standard', - ultimate: 'ultimate', - creative: 'creative', -}; +const PROFILE_IDS: KnownHarnessProfileId[] = [...HARNESS_IDS, 'other']; +type DensityHarnessProfileId = 'Minimal' | 'Standard' | 'Ultimate'; function isDensityProfile(profile: KnownHarnessProfileId): profile is DensityHarnessProfileId { - return profile === 'minimal' || profile === 'balanced' || profile === 'ultimate'; + return profile === 'Minimal' || profile === 'Standard' || profile === 'Ultimate'; } function isSelectableProfile( profile: KnownHarnessProfileId, ): profile is SelectableHarnessProfileId { - return isDensityProfile(profile) || profile === 'creative'; + return isDensityProfile(profile) || profile === 'Creative'; } function sameAgent(left: string | null | undefined, right: string | null | undefined): boolean { - return left?.trim().toLowerCase() === right?.trim().toLowerCase(); + return canonicalAgentId(left?.trim() ?? '').toLowerCase() === canonicalAgentId(right?.trim() ?? '').toLowerCase(); } /** Menu rows and the compact add-menu trigger share one mode mark. */ @@ -105,7 +84,7 @@ function HarnessProfileMark({ {profile === 'other' ? ( @@ -116,7 +95,7 @@ function HarnessProfileMark({ /> ) : ( diff --git a/src/web-ui/src/flow_chat/components/ModelSelector.scss b/src/web-ui/src/flow_chat/components/ModelSelector.scss index edaebe22be..7d1527f5f2 100644 --- a/src/web-ui/src/flow_chat/components/ModelSelector.scss +++ b/src/web-ui/src/flow_chat/components/ModelSelector.scss @@ -50,10 +50,6 @@ opacity: 1; } - &:active:not(:disabled) { - transform: scale(0.97); - } - &:focus-visible { border-color: color-mix(in srgb, var(--openbitfun-color-accent-hover) 40%, transparent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--openbitfun-color-accent-default) 20%, transparent); diff --git a/src/web-ui/src/flow_chat/components/ModelSelectorExternal.test.tsx b/src/web-ui/src/flow_chat/components/ModelSelectorExternal.test.tsx index 2e64797e8c..1310853d63 100644 --- a/src/web-ui/src/flow_chat/components/ModelSelectorExternal.test.tsx +++ b/src/web-ui/src/flow_chat/components/ModelSelectorExternal.test.tsx @@ -203,7 +203,7 @@ describe('ModelSelector external transport reuse', () => { workspacePath: '/tmp/miniapp-runtime', projectWorkspacePath: '/tmp/project', config: { - agentType: 'agentic', + agentType: 'Standard', modelName: 'model-a', reasoningPreset: 'medium', }, @@ -274,7 +274,7 @@ describe('ModelSelector external transport reuse', () => { await act(async () => { root.render( { .mockResolvedValueOnce(updatedCatalog); await act(async () => { - root.render(); + root.render(); await Promise.resolve(); }); expect(catalogUpdated).toBeTypeOf('function'); @@ -401,7 +401,7 @@ describe('ModelSelector external transport reuse', () => { await act(async () => { root.render( { await act(async () => { root.render( { await act(async () => { root.render( { await act(async () => { root.render( { await act(async () => { root.render( { await act(async () => { root.render( { }); const renderTarget = async (selection: ExternalModelSelection) => { await act(async () => { - root.render(); + root.render(); }); }; const clickControl = async (testId: string) => { diff --git a/src/web-ui/src/flow_chat/components/ModelSelectorProviderLevels.test.tsx b/src/web-ui/src/flow_chat/components/ModelSelectorProviderLevels.test.tsx index ec5d0d7cb7..1f301aa392 100644 --- a/src/web-ui/src/flow_chat/components/ModelSelectorProviderLevels.test.tsx +++ b/src/web-ui/src/flow_chat/components/ModelSelectorProviderLevels.test.tsx @@ -173,7 +173,7 @@ describe('ModelSelector provider levels', () => { await act(async () => { root.render( , @@ -221,7 +221,7 @@ describe('ModelSelector provider levels', () => { it('opens with model and reasoning settings while omitting speed and reset actions', async () => { flowChatStoreMocks.sessions.set('session-a', { config: { - agentType: 'agentic', + agentType: 'Standard', modelName: 'umbra-main', reasoningPreset: 'high', }, @@ -279,7 +279,7 @@ describe('ModelSelector provider levels', () => { it('opens generic reasoning defaults from the settings summary', async () => { flowChatStoreMocks.sessions.set('session-a', { - config: { agentType: 'agentic', modelName: 'umbra-main', reasoningPreset: 'high' }, + config: { agentType: 'Standard', modelName: 'umbra-main', reasoningPreset: 'high' }, }); aiApiMocks.getModelCatalog.mockResolvedValue({ version: 1, @@ -491,7 +491,7 @@ describe('ModelSelector provider levels', () => { await act(async () => { root.render( , ); @@ -530,7 +530,7 @@ describe('ModelSelector provider levels', () => { await act(async () => { root.render( , ); @@ -563,7 +563,7 @@ describe('ModelSelector provider levels', () => { await act(async () => { root.render( , ); @@ -594,7 +594,7 @@ describe('ModelSelector provider levels', () => { await act(async () => { root.render( , @@ -634,7 +634,7 @@ describe('ModelSelector provider levels', () => { await act(async () => { root.render( , ); @@ -690,7 +690,7 @@ describe('ModelSelector provider levels', () => { it('opens native submenus only by click, keeps the parent stable, and toggles them explicitly', async () => { flowChatStoreMocks.sessions.set('session-a', { - config: { agentType: 'agentic', modelName: 'umbra-main', reasoningPreset: 'high' }, + config: { agentType: 'Standard', modelName: 'umbra-main', reasoningPreset: 'high' }, }); aiApiMocks.getModelCatalog.mockResolvedValue({ version: 1, @@ -750,7 +750,7 @@ describe('ModelSelector provider levels', () => { it('supports Right and Left Arrow navigation and closes both menus on outside click', async () => { flowChatStoreMocks.sessions.set('session-a', { - config: { agentType: 'agentic', modelName: 'umbra-main', reasoningPreset: 'high' }, + config: { agentType: 'Standard', modelName: 'umbra-main', reasoningPreset: 'high' }, }); aiApiMocks.getModelCatalog.mockResolvedValue({ version: 1, diff --git a/src/web-ui/src/flow_chat/components/PlannerButton.scss b/src/web-ui/src/flow_chat/components/PlannerButton.scss index 9d88d7d0da..f831897ca3 100644 --- a/src/web-ui/src/flow_chat/components/PlannerButton.scss +++ b/src/web-ui/src/flow_chat/components/PlannerButton.scss @@ -82,11 +82,6 @@ } } - // Active state - &:active { - transform: scale(0.97); - } - &__dot { width: 5px; height: 5px; diff --git a/src/web-ui/src/flow_chat/components/RichTextInput.scss b/src/web-ui/src/flow_chat/components/RichTextInput.scss index ac2d1fa98b..7e289c8a18 100644 --- a/src/web-ui/src/flow_chat/components/RichTextInput.scss +++ b/src/web-ui/src/flow_chat/components/RichTextInput.scss @@ -292,10 +292,6 @@ opacity: 1; } - &:active { - transform: scale(0.97); - } - &:focus-visible { outline: 2px solid color-mix(in srgb, var(--openbitfun-color-accent-hover) 40%, transparent); outline-offset: 1px; diff --git a/src/web-ui/src/flow_chat/components/ScrollToBottomButton.scss b/src/web-ui/src/flow_chat/components/ScrollToBottomButton.scss index 6dad18c39e..345f083ec9 100644 --- a/src/web-ui/src/flow_chat/components/ScrollToBottomButton.scss +++ b/src/web-ui/src/flow_chat/components/ScrollToBottomButton.scss @@ -49,10 +49,6 @@ pointer-events: none; } - &:active { - transform: scale(0.97); - } - &:focus-visible { outline: 3px solid var(--openbitfun-color-content-primary); outline-offset: 3px; diff --git a/src/web-ui/src/flow_chat/components/ScrollToLatestBar.scss b/src/web-ui/src/flow_chat/components/ScrollToLatestBar.scss index 60ab32077c..2f150c8d97 100644 --- a/src/web-ui/src/flow_chat/components/ScrollToLatestBar.scss +++ b/src/web-ui/src/flow_chat/components/ScrollToLatestBar.scss @@ -103,13 +103,6 @@ opacity 120ms ease; } - // ========== Active state ========== - &:active { - .scroll-to-latest-bar__btn { - transform: scale(0.97); - } - } - // ========== Focus state ========== &:focus-visible { outline: none; diff --git a/src/web-ui/src/flow_chat/components/ScrollToTurnHeaderButton.scss b/src/web-ui/src/flow_chat/components/ScrollToTurnHeaderButton.scss index 5551e1b5bd..7535ba1c87 100644 --- a/src/web-ui/src/flow_chat/components/ScrollToTurnHeaderButton.scss +++ b/src/web-ui/src/flow_chat/components/ScrollToTurnHeaderButton.scss @@ -75,10 +75,6 @@ transform: translateY(-1px); } - &:active { - transform: scale(0.97); - } - &:focus-visible { outline: none; border-color: var(--openbitfun-color-border-strong); diff --git a/src/web-ui/src/flow_chat/components/ToolApprovalBar.scss b/src/web-ui/src/flow_chat/components/ToolApprovalBar.scss index cef287b88b..939735a4d2 100644 --- a/src/web-ui/src/flow_chat/components/ToolApprovalBar.scss +++ b/src/web-ui/src/flow_chat/components/ToolApprovalBar.scss @@ -108,12 +108,6 @@ background: color-mix(in srgb, var(--openbitfun-color-status-danger-emphasis) 16%, transparent); } -.tool-approval-bar__instruction-submit:active:not(:disabled), -.tool-approval-bar__icon-button:active:not(:disabled), -.tool-approval-bar__auto-execute-button:active:not(:disabled) { - transform: scale(0.97); -} - .tool-approval-bar__instruction-submit:focus-visible, .tool-approval-bar__icon-button:focus-visible, .tool-approval-bar__auto-execute-button:focus-visible { diff --git a/src/web-ui/src/flow_chat/components/WelcomePanel.css b/src/web-ui/src/flow_chat/components/WelcomePanel.css index 63768a34a0..b33508f73b 100644 --- a/src/web-ui/src/flow_chat/components/WelcomePanel.css +++ b/src/web-ui/src/flow_chat/components/WelcomePanel.css @@ -102,10 +102,6 @@ outline-offset: 1px; } -.welcome-panel__inline-btn:active:not(:disabled) { - transform: scale(0.97); -} - .welcome-panel__inline-btn--active { background: var(--openbitfun-color-surface-subtle); } diff --git a/src/web-ui/src/flow_chat/components/WelcomePanel.test.tsx b/src/web-ui/src/flow_chat/components/WelcomePanel.test.tsx index 3f89622981..3790dc3022 100644 --- a/src/web-ui/src/flow_chat/components/WelcomePanel.test.tsx +++ b/src/web-ui/src/flow_chat/components/WelcomePanel.test.tsx @@ -93,7 +93,7 @@ describe('WelcomePanel Git summary loading', () => { gitApiMock.isGitRepository.mockReturnValue(repositoryProbe.promise); await act(async () => { - root.render(); + root.render(); }); expect(gitApiMock.isGitRepository).toHaveBeenCalledWith('D:/workspace/OpenBitFun'); @@ -114,7 +114,7 @@ describe('WelcomePanel Git summary loading', () => { gitApiMock.isGitRepository.mockResolvedValue(true); await act(async () => { - root.render(); + root.render(); }); expect(gitApiMock.getStatus).toHaveBeenCalledWith('D:/workspace/OpenBitFun', 'welcome_panel'); @@ -134,7 +134,7 @@ describe('WelcomePanel Git summary loading', () => { it('portals the workspace menu outside the scrollable welcome panel', async () => { gitApiMock.isGitRepository.mockResolvedValue(false); await act(async () => { - root.render(); + root.render(); }); const trigger = container.querySelector('[data-openbitfun-part="workspaceAction"]'); diff --git a/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.review-action.test.tsx b/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.review-action.test.tsx index a0b954a1cb..6f05c6da05 100644 --- a/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.review-action.test.tsx +++ b/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.review-action.test.tsx @@ -663,7 +663,7 @@ describe('BtwSessionPanel review action bar integration', () => { sessionId: 'deep-review-child', toolCallId: 'direct-child-tool', projectId: 'project-1', - agentId: 'agentic', + agentId: 'Standard', action: 'edit', resources: ['src/main.rs'], source: { kind: 'tool_call', identity: 'Write' }, diff --git a/src/web-ui/src/flow_chat/components/modern/FlowChatTurnRail.scss b/src/web-ui/src/flow_chat/components/modern/FlowChatTurnRail.scss index 910ba58e77..ca262c81dc 100644 --- a/src/web-ui/src/flow_chat/components/modern/FlowChatTurnRail.scss +++ b/src/web-ui/src/flow_chat/components/modern/FlowChatTurnRail.scss @@ -59,8 +59,7 @@ width 140ms ease, height 140ms ease, background-color 140ms ease, - opacity 140ms ease, - transform 100ms ease; + opacity 140ms ease; } // Hover previews a target without changing the actual current step. Only @@ -91,10 +90,6 @@ opacity: 1; } - &__item:active &__bar { - transform: translateY(-50%) scaleX(0.92); - } - &__tooltip-content { min-width: 160px; max-width: 260px; diff --git a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.scss b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.scss index bd2e10a1f2..0324dd4347 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.scss +++ b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.scss @@ -105,10 +105,6 @@ color: var(--openbitfun-color-content-primary); } - &:active { - transform: scale(0.98); - } - &:focus-visible { outline: 2px solid color-mix(in srgb, var(--openbitfun-color-accent-hover) 40%, transparent); outline-offset: 2px; @@ -249,10 +245,6 @@ color: var(--openbitfun-color-content-primary); } - &:active { - transform: scale(0.97); - } - &:focus-visible { outline: 2px solid color-mix(in srgb, var(--openbitfun-color-accent-hover) 40%, transparent); outline-offset: 1px; diff --git a/src/web-ui/src/flow_chat/components/modern/SessionFileModificationsBar.scss b/src/web-ui/src/flow_chat/components/modern/SessionFileModificationsBar.scss index dd94779ed1..cb76e1f936 100644 --- a/src/web-ui/src/flow_chat/components/modern/SessionFileModificationsBar.scss +++ b/src/web-ui/src/flow_chat/components/modern/SessionFileModificationsBar.scss @@ -99,10 +99,6 @@ color: var(--openbitfun-color-content-primary); } - &:active:not(:disabled) { - transform: scale(0.95); - } - &:disabled { opacity: 0.4; cursor: not-allowed; diff --git a/src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.scss b/src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.scss index 1755133555..7234fae1ec 100644 --- a/src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.scss +++ b/src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.scss @@ -44,7 +44,6 @@ &:active:not(:disabled) { background: var(--openbitfun-color-action-neutral-surface); - transform: scale(0.98); } &:disabled { diff --git a/src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss b/src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss index 68365ecd4a..c8927a9706 100644 --- a/src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss +++ b/src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss @@ -233,10 +233,6 @@ box-shadow: none; } - &:active:not(:disabled) { - transform: scale(0.97); - } - &:focus-visible { outline: 2px solid color-mix(in srgb, var(--openbitfun-color-accent-hover) 40%, transparent); outline-offset: 1px; @@ -383,10 +379,6 @@ background: color-mix(in srgb, var(--openbitfun-color-content-on-dark) 30%, transparent); } - &:active { - transform: scale(0.97); - } - &:focus-visible { outline: 2px solid var(--openbitfun-color-content-on-dark); outline-offset: 2px; diff --git a/src/web-ui/src/flow_chat/components/modern/permissionRequestRouting.test.ts b/src/web-ui/src/flow_chat/components/modern/permissionRequestRouting.test.ts index 465a3503ec..cce6f42a36 100644 --- a/src/web-ui/src/flow_chat/components/modern/permissionRequestRouting.test.ts +++ b/src/web-ui/src/flow_chat/components/modern/permissionRequestRouting.test.ts @@ -27,7 +27,7 @@ function request( sessionId, toolCallId, projectId: 'project-1', - agentId: parentSessionId ? 'Explore' : 'agentic', + agentId: parentSessionId ? 'Explore' : 'Standard', action: 'edit', resources: ['src/main.rs'], source: { kind: 'tool_call', identity: 'Write' }, diff --git a/src/web-ui/src/flow_chat/components/modern/usePermissionRequests.test.tsx b/src/web-ui/src/flow_chat/components/modern/usePermissionRequests.test.tsx index 5732b0b700..2d47d0e1c1 100644 --- a/src/web-ui/src/flow_chat/components/modern/usePermissionRequests.test.tsx +++ b/src/web-ui/src/flow_chat/components/modern/usePermissionRequests.test.tsx @@ -65,7 +65,7 @@ function request( sessionId, toolCallId: `${requestId}-tool`, projectId: 'project-1', - agentId: parentSessionId ? 'Explore' : 'agentic', + agentId: parentSessionId ? 'Explore' : 'Standard', action: 'edit', resources: ['src/main.rs'], source: { kind: 'tool_call', identity: 'Write' }, @@ -308,7 +308,7 @@ describe('usePermissionRequests', () => { displayName: 'build-host', }, title: 'Remote task', - agentType: 'agentic', + agentType: 'Standard', approvalPolicy: 'remote', cursor: 0, state: 'running', diff --git a/src/web-ui/src/flow_chat/components/session-menu/SessionMenu.scss b/src/web-ui/src/flow_chat/components/session-menu/SessionMenu.scss index df86ae1401..d6af7c3607 100644 --- a/src/web-ui/src/flow_chat/components/session-menu/SessionMenu.scss +++ b/src/web-ui/src/flow_chat/components/session-menu/SessionMenu.scss @@ -22,7 +22,6 @@ $session-menu-timing: cubic-bezier(0.4, 0, 0.2, 1); border-radius: var(--openbitfun-radius-sm); cursor: pointer; transition: - transform 120ms cubic-bezier(0.23, 1, 0.32, 1), background-color 160ms ease, color 160ms ease, opacity 160ms ease; @@ -30,11 +29,6 @@ $session-menu-timing: cubic-bezier(0.4, 0, 0.2, 1); &:hover:not(:disabled) { background: var(--openbitfun-color-action-secondary-hover); color: var(--openbitfun-color-accent-default); - transform: scale(1.1); - } - - &:active:not(:disabled) { - transform: scale(0.95); } &:disabled { diff --git a/src/web-ui/src/flow_chat/components/toolbar-mode/ToolbarMode.scss b/src/web-ui/src/flow_chat/components/toolbar-mode/ToolbarMode.scss index 4cbcd5921a..7e31c4a6e2 100644 --- a/src/web-ui/src/flow_chat/components/toolbar-mode/ToolbarMode.scss +++ b/src/web-ui/src/flow_chat/components/toolbar-mode/ToolbarMode.scss @@ -138,7 +138,6 @@ $transition-timing: cubic-bezier(0.4, 0, 0.2, 1); border-radius: var(--openbitfun-radius-sm); cursor: pointer; transition: - transform 120ms cubic-bezier(0.23, 1, 0.32, 1), background-color 160ms ease, color 160ms ease, opacity 160ms ease; @@ -146,11 +145,6 @@ $transition-timing: cubic-bezier(0.4, 0, 0.2, 1); &:hover:not(:disabled) { background: var(--openbitfun-color-action-secondary-hover); color: var(--openbitfun-color-accent-default); - transform: scale(1.1); - } - - &:active:not(:disabled) { - transform: scale(0.95); } &:disabled { @@ -522,10 +516,6 @@ $transition-timing: cubic-bezier(0.4, 0, 0.2, 1); color: var(--openbitfun-color-content-primary); } - &:active { - transform: scale(0.95); - } - &--confirm { background: var(--openbitfun-color-status-success-surface); color: var(--openbitfun-color-status-success-content); diff --git a/src/web-ui/src/flow_chat/components/voice/ConversationModeSurface.scss b/src/web-ui/src/flow_chat/components/voice/ConversationModeSurface.scss index 8bc280ae84..5f705bea02 100644 --- a/src/web-ui/src/flow_chat/components/voice/ConversationModeSurface.scss +++ b/src/web-ui/src/flow_chat/components/voice/ConversationModeSurface.scss @@ -60,7 +60,6 @@ &:active:not(:disabled) { background: var(--openbitfun-color-action-neutral-surface-hover); - transform: scale(0.985); } &:focus-visible { diff --git a/src/web-ui/src/flow_chat/components/voice/voiceTaskBridge.ts b/src/web-ui/src/flow_chat/components/voice/voiceTaskBridge.ts index 8daaea5912..4ffd62ba40 100644 --- a/src/web-ui/src/flow_chat/components/voice/voiceTaskBridge.ts +++ b/src/web-ui/src/flow_chat/components/voice/voiceTaskBridge.ts @@ -552,7 +552,7 @@ export async function runOpenBitFunVoiceTask( normalizedTask, sessionId, normalizedTask, - 'agentic', + 'Standard', undefined, { userMessageMetadata: { source: 'realtime_voice' } }, ) diff --git a/src/web-ui/src/flow_chat/deep-review/action-bar/DeepReviewActionBar.tsx b/src/web-ui/src/flow_chat/deep-review/action-bar/DeepReviewActionBar.tsx index 5d37a18d74..41fff793d8 100644 --- a/src/web-ui/src/flow_chat/deep-review/action-bar/DeepReviewActionBar.tsx +++ b/src/web-ui/src/flow_chat/deep-review/action-bar/DeepReviewActionBar.tsx @@ -512,7 +512,7 @@ export const ReviewActionBar: React.FC = ({ childSessionId : 'Start fixing review findings', }), 'ReviewFixer', - 'agentic', + 'Standard', ); } catch (error) { log.error('Failed to start review remediation', { childSessionId, reviewMode, error }); @@ -715,7 +715,7 @@ export const ReviewActionBar: React.FC = ({ childSessionId count: retryableSlices.length, }), 'DeepReview', - 'agentic', + 'Standard', ); store.minimize(childSessionId); } catch (error) { diff --git a/src/web-ui/src/flow_chat/hooks/useAcpSlashCommands.test.ts b/src/web-ui/src/flow_chat/hooks/useAcpSlashCommands.test.ts index 85ef1411e3..64aaefbe91 100644 --- a/src/web-ui/src/flow_chat/hooks/useAcpSlashCommands.test.ts +++ b/src/web-ui/src/flow_chat/hooks/useAcpSlashCommands.test.ts @@ -50,8 +50,8 @@ describe('acpSessionRef', () => { expect( acpSessionRef({ sessionId: 's1', - config: { agentType: 'agentic' }, - mode: 'agentic', + config: { agentType: 'Standard' }, + mode: 'Standard', } as never), ).toBeNull(); }); diff --git a/src/web-ui/src/flow_chat/hooks/useFlowChat.ts b/src/web-ui/src/flow_chat/hooks/useFlowChat.ts index 588d843a0e..b475aec982 100644 --- a/src/web-ui/src/flow_chat/hooks/useFlowChat.ts +++ b/src/web-ui/src/flow_chat/hooks/useFlowChat.ts @@ -75,7 +75,7 @@ export const useFlowChat = () => { const remoteConnectionId = isRemote ? workspace?.connectionId : undefined; const remoteSshHost = isRemote ? workspace?.sshHost : undefined; - const agentTypeForSession = (config?.agentType || 'agentic').trim() || 'agentic'; + const agentTypeForSession = (config?.agentType || 'Standard').trim() || 'Standard'; const maxContextTokens = await getModelMaxTokens(config?.modelName, agentTypeForSession); const sessionTitleMode = workspace?.workspaceKind === WorkspaceKind.Assistant @@ -156,7 +156,7 @@ export const useFlowChat = () => { try { await aiApi.createAISession({ - agent_type: config?.agentType || 'agentic', + agent_type: config?.agentType || 'Standard', model_name: config?.modelName || 'default', description: `FlowChat session ${sessionId}` }); @@ -170,7 +170,7 @@ export const useFlowChat = () => { workspaceId: workspace?.id ?? config?.workspaceId, }; - const fallbackAgentType = (config?.agentType || 'agentic').trim() || 'agentic'; + const fallbackAgentType = (config?.agentType || 'Standard').trim() || 'Standard'; const fallbackTitleMode = workspace?.workspaceKind === WorkspaceKind.Assistant ? 'claw' diff --git a/src/web-ui/src/flow_chat/hooks/useMessageSender.test.tsx b/src/web-ui/src/flow_chat/hooks/useMessageSender.test.tsx index 508e2b0865..38db9f3cd8 100644 --- a/src/web-ui/src/flow_chat/hooks/useMessageSender.test.tsx +++ b/src/web-ui/src/flow_chat/hooks/useMessageSender.test.tsx @@ -15,7 +15,7 @@ const mocks = vi.hoisted(() => { createChatSession, sendMessage, getFlowChatState: () => ({ - sessions: new Map([['created-session', { mode: 'agentic' }]]), + sessions: new Map([['created-session', { mode: 'Standard' }]]), }), }; @@ -110,13 +110,13 @@ describe('useMessageSender', () => { { workspacePath: '/workspace/project', }, - 'agentic', + 'Standard', ); expect(mocks.sendMessage).toHaveBeenCalledWith( 'hello', 'created-session', 'hello', - 'agentic', + 'Standard', undefined, expect.objectContaining({ pendingQueueDraft: { @@ -141,7 +141,7 @@ describe('useMessageSender', () => { 'expanded paste content', 'created-session', '[Pasted text #1]', - 'agentic', + 'Standard', undefined, expect.objectContaining({ pendingQueueDraft: { diff --git a/src/web-ui/src/flow_chat/hooks/useMessageSender.ts b/src/web-ui/src/flow_chat/hooks/useMessageSender.ts index fa7cc5d7c4..60a4135d5d 100644 --- a/src/web-ui/src/flow_chat/hooks/useMessageSender.ts +++ b/src/web-ui/src/flow_chat/hooks/useMessageSender.ts @@ -141,18 +141,18 @@ export function useMessageSender(props: UseMessageSenderProps): UseMessageSender textLength: trimmedMessage.length, contextCount: contexts.length, hasSession: !!sessionId, - agentType: currentAgentType || 'agentic', + agentType: currentAgentType || 'Standard', }); try { const flowChatManager = FlowChatManager.getInstance(); - let agentTypeForSend = currentAgentType || 'agentic'; + let agentTypeForSend = currentAgentType || 'Standard'; if (options?.execution?.kind === 'fresh_external_subagent' && contexts.length > 0) { throw new Error('External subagent command delegation does not accept composer context'); } if (!sessionId) { - const agentType = currentAgentType || 'agentic'; + const agentType = currentAgentType || 'Standard'; const sessionConfig = flowChatSessionConfigForCurrentWorkspace(); sessionId = await flowChatManager.createChatSession(sessionConfig, agentType); @@ -286,7 +286,7 @@ export function useMessageSender(props: UseMessageSenderProps): UseMessageSender } catch (error) { log.error('Failed to send message', { sessionId, - agentType: currentAgentType || 'agentic', + agentType: currentAgentType || 'Standard', contextCount: contexts.length, error: (error as Error)?.message ?? 'unknown', }); diff --git a/src/web-ui/src/flow_chat/hooks/useSessionModeSelection.test.tsx b/src/web-ui/src/flow_chat/hooks/useSessionModeSelection.test.tsx index 19923437c3..37e238bc87 100644 --- a/src/web-ui/src/flow_chat/hooks/useSessionModeSelection.test.tsx +++ b/src/web-ui/src/flow_chat/hooks/useSessionModeSelection.test.tsx @@ -38,10 +38,10 @@ function Probe({ return ( <> {String(selection.isModeChangePending)} - -