@kortix/ui 0.14.7 → 0.14.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/_vendor/kortix-shared.js +1 -1
  2. package/dist/_vendor/shared/runtime-versions.d.ts +4 -0
  3. package/dist/access/access/access-dialog-share.js +4 -7
  4. package/dist/access/access/access-dialog.js +2 -1
  5. package/dist/access/access/principal-picker.js +6 -5
  6. package/dist/customize/agents/agent-cover-card.d.ts +17 -0
  7. package/dist/customize/agents/agent-cover-card.js +13 -0
  8. package/dist/customize/agents/agent-page.js +6 -33
  9. package/dist/customize/agents/agents-page.js +43 -49
  10. package/dist/customize/migrate-to-v2/migration-prompt.d.ts +1 -0
  11. package/dist/customize/migrate-to-v2/migration-prompt.js +1 -0
  12. package/dist/customize/sections/view/channels-view.js +2 -6
  13. package/dist/customize/shared/new-entity-menu.js +4 -1
  14. package/dist/customize/skills/skills-page.js +4 -5
  15. package/dist/customize/triggers/schedule/then-fields.js +1 -5
  16. package/dist/customize/triggers/schedule-view.js +4 -2
  17. package/dist/i18n/en.d.ts +60 -12
  18. package/dist/i18n/en.js +62 -14
  19. package/dist/internal/components/agent-mark.d.ts +38 -0
  20. package/dist/internal/components/agent-mark.js +38 -0
  21. package/dist/internal/lib/pixel-mark.d.ts +33 -0
  22. package/dist/internal/lib/pixel-mark.js +152 -0
  23. package/dist/internal/lib/use-project-feature-flags.js +3 -3
  24. package/dist/markdown/genui/components/inline.d.ts +1 -1
  25. package/dist/markdown/setup-links/app-credentials-form.d.ts +12 -0
  26. package/dist/markdown/setup-links/app-credentials-form.js +115 -0
  27. package/dist/markdown/setup-links/secret-intake-form.d.ts +10 -0
  28. package/dist/markdown/setup-links/secret-intake-form.js +1 -1
  29. package/dist/primitives/entity-avatar.d.ts +4 -1
  30. package/dist/primitives/entity-avatar.js +4 -2
  31. package/dist/primitives/user-avatar.d.ts +3 -1
  32. package/dist/primitives/user-avatar.js +4 -2
  33. package/dist/providers/ui-provider.d.ts +1 -1
  34. package/dist/session/composer/agent-selector.d.ts +7 -1
  35. package/dist/session/composer/agent-selector.js +20 -10
  36. package/dist/session/composer/context-usage.d.ts +5 -0
  37. package/dist/session/composer/context-usage.js +6 -1
  38. package/dist/session/composer/token-progress.js +4 -4
  39. package/dist/session/header/export-transcript-modal.d.ts +3 -2
  40. package/dist/session/header/export-transcript-modal.js +6 -3
  41. package/dist/session/models/model-selector.d.ts +3 -1
  42. package/dist/session/models/model-selector.js +3 -2
  43. package/dist/session/panel/easy/plan-card.d.ts +2 -0
  44. package/dist/session/panel/easy/plan-card.js +2 -0
  45. package/dist/session/thread/plan-surface.d.ts +1 -0
  46. package/dist/session/thread/plan-surface.js +1 -0
  47. package/dist/session/thread/session-chat.js +2 -2
  48. package/dist/session/thread/session-context-metrics.d.ts +15 -0
  49. package/dist/session/thread/session-context-metrics.js +10 -1
  50. package/dist/session/thread/session-context-modal.d.ts +4 -2
  51. package/dist/session/thread/session-context-modal.js +16 -9
  52. package/dist/session/thread/transcript.d.ts +4 -0
  53. package/dist/session/thread/transcript.js +18 -5
  54. package/dist/session/thread/turn-share.d.ts +16 -0
  55. package/dist/session/thread/turn-share.js +162 -0
  56. package/dist/session/tool/tools/apply-patch-tool.d.ts +1 -0
  57. package/dist/session/tool/tools/apply-patch-tool.js +1 -0
  58. package/dist/session/tool/tools/pty-kill-tool.d.ts +1 -0
  59. package/dist/session/tool/tools/pty-kill-tool.js +1 -0
  60. package/dist/session/tool/tools/pty-read-tool.d.ts +2 -0
  61. package/dist/session/tool/tools/pty-read-tool.js +2 -0
  62. package/dist/session/tool/tools/pty-spawn-tool.d.ts +1 -0
  63. package/dist/session/tool/tools/pty-spawn-tool.js +1 -0
  64. package/dist/session/tool/tools/pty-write-tool.d.ts +1 -0
  65. package/dist/session/tool/tools/pty-write-tool.js +1 -0
  66. package/dist/session/tool/tools/todo-write-tool.d.ts +1 -0
  67. package/dist/session/tool/tools/todo-write-tool.js +1 -0
  68. package/dist/session/turn/message-author-label.js +6 -2
  69. package/dist/session/turn/plan-anchor.d.ts +3 -0
  70. package/dist/session/turn/plan-anchor.js +3 -0
  71. package/dist/session/turn/plan-card.d.ts +7 -0
  72. package/dist/session/turn/plan-card.js +7 -0
  73. package/dist/styles.css +1 -1
  74. package/dist/terminal/session-terminal-connect-bar.js +2 -1
  75. package/dist/workspace/sidebar/session-brief-hover-card.js +28 -1
  76. package/package.json +11 -3
  77. package/dist/customize/agents/agent-nav.prototype.d.ts +0 -35
  78. package/dist/customize/agents/agent-nav.prototype.js +0 -95
@@ -7622,7 +7622,7 @@ function avatarFill(seed) {
7622
7622
  var NEW_PROMPTS = {
7623
7623
  agent: "I want to configure a new agent for this project. Ask me what it should specialize in and how it should behave, then create its config at `agents/<name>.md`, declare it in `kortix.yaml` under `agents:` with `file: agents/<name>.md` and the access it needs, and open a change request so I can review and merge it.",
7624
7624
  skill: "I want to add a new skill to this project. Ask me what capability it should provide and when it should trigger, then scaffold `skills/<name>/SKILL.md` and open a change request so I can review and merge it.",
7625
- command: "I want to create a new slash command for this project. Ask me what it should do, then add it at `harnesses/opencode/commands/<name>.md` and open a change request so I can review and merge it.",
7625
+ command: "I want to create a new slash command for this project. Ask me what it should do, then add it where pi reads commands (`commands:` in `kortix.yaml` on `kortix_version: 3`, else a prompt template at `harnesses/pi/prompts/<name>.md`) and open a change request so I can review and merge it.",
7626
7626
  connector: "I want to connect an outside service to this project. Ask me which service and what the agents should be able to do with it, then add the connector with the Kortix CLI and tell me what still needs authorizing.",
7627
7627
  secret: "I want to add a secret to this project. Ask me what it is for and which agents need it \u2014 never ask me to paste the value in chat \u2014 then create the secret with the Kortix CLI, grant it to those agents, and tell me where to enter the value.",
7628
7628
  trigger: "I want to set up a trigger for this project. Ask me what should run, which agent should run it, and on what schedule or event, then create it with the Kortix CLI and show me the result."
@@ -12,7 +12,9 @@ export type RuntimeVersions = {
12
12
  bun: string;
13
13
  bunSha256Amd64: string;
14
14
  bunSha256Arm64: string;
15
+ /** @deprecated OpenCode only. OpenCode is sunset (2026-10-23): no new code. See apps/kortix-sandbox-agent-server/src/harness/README.md "OpenCode sunset". */
15
16
  opencode: string;
17
+ /** @deprecated OpenCode only. OpenCode is sunset (2026-10-23): no new code. See apps/kortix-sandbox-agent-server/src/harness/README.md "OpenCode sunset". */
16
18
  opencodeSdk: string;
17
19
  agentBrowser: string;
18
20
  playwright: string;
@@ -35,7 +37,9 @@ export declare const PYTHON_PACKAGE_FLOOR_IMPORTS: Record<string, string>;
35
37
  export declare const BUN_VERSION: string;
36
38
  export declare const BUN_SHA256_AMD64: string;
37
39
  export declare const BUN_SHA256_ARM64: string;
40
+ /** @deprecated OpenCode only. OpenCode is sunset (2026-10-23): no new code. See apps/kortix-sandbox-agent-server/src/harness/README.md "OpenCode sunset". */
38
41
  export declare const OPENCODE_VERSION: string;
42
+ /** @deprecated OpenCode only. OpenCode is sunset (2026-10-23): no new code. See apps/kortix-sandbox-agent-server/src/harness/README.md "OpenCode sunset". */
39
43
  export declare const OPENCODE_SDK_VERSION: string;
40
44
  export declare const OPENCODE_USER_AGENT: string;
41
45
  export declare const AGENT_BROWSER_VERSION: string;
@@ -13,6 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  //
14
14
  // There is no expiry field here on purpose: when the last narrowing grant
15
15
  // expires, the account silently widens to everyone.
16
+ import { AgentMark } from '../../internal/components/agent-mark.js';
16
17
  import { Button } from '../../primitives/button.js';
17
18
  import { EntityAvatar } from '../../primitives/entity-avatar.js';
18
19
  import { Field, FieldLabel } from '../../primitives/field.js';
@@ -25,7 +26,7 @@ import { useTranslations as useI18nTranslations } from '../../i18n/index.js';
25
26
  import { cn } from '../../internal/lib/utils.js';
26
27
  import { createAssignment, revokeAssignment, shareConnection, } from '@kortix/sdk';
27
28
  import { invalidatePermissionProbes, qk } from '@kortix/sdk/react';
28
- import { LockIcon, RobotIcon, UsersIcon, UsersThreeIcon, XIcon } from '@phosphor-icons/react';
29
+ import { LockIcon, UsersIcon, UsersThreeIcon, XIcon } from '@phosphor-icons/react';
29
30
  import { useMutation, useQueryClient } from '@tanstack/react-query';
30
31
  import { useState } from 'react';
31
32
  import { EMPTY_PRINCIPAL_SELECTION, PrincipalPicker, } from './principal-picker.js';
@@ -176,14 +177,10 @@ export function ShareAccessBody({ open, onOpenChange, accountId, projectId, proj
176
177
  if (save.isPending)
177
178
  return;
178
179
  onOpenChange(next);
179
- }, children: _jsxs(ModalContent, { className: "sm:max-w-md", children: [_jsxs(ModalHeader, { children: [_jsx(ModalTitle, { children: t('shareTitle', { label: object.label }) }), _jsx(ModalDescription, { children: privateOwner ? t('sharePrivateDescription') : t('shareDescription') })] }), _jsxs(ModalBody, { className: "max-h-[60vh] space-y-4 overflow-y-auto", children: [_jsxs(Field, { className: "gap-1.5", children: [_jsx(FieldLabel, { children: t('whoCanUse') }), _jsx("ul", { className: "space-y-2", "data-testid": "share-audience", children: privateOwner ? (_jsx(AudienceRow, { avatar: _jsx(UserAvatar, { email: privateOwner.label, size: "sm" }), label: privateOwner.label, meta: keepOwner ? t('ownerMeta') : t('removedMeta'), removed: !keepOwner, action: keepOwner ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-8 shrink-0", "aria-label": t('remove', { name: privateOwner.label }), disabled: save.isPending, onClick: () => setKeepOwner(false), children: _jsx(XIcon, { className: "size-3.5" }) })) : (_jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: save.isPending, onClick: () => setKeepOwner(true), children: t('keep') })) })) : current.length === 0 ? (_jsx(AudienceRow, { avatar: _jsx(EntityAvatar, { icon: UsersThreeIcon, label: everyoneLabel, size: "sm" }), label: everyoneLabel, meta: t('everyoneDefaultMeta') })) : (current.map((grant) => {
180
+ }, children: _jsxs(ModalContent, { className: "sm:max-w-md", children: [_jsxs(ModalHeader, { children: [_jsx(ModalTitle, { children: t('shareTitle', { label: object.label }) }), _jsx(ModalDescription, { children: privateOwner ? t('sharePrivateDescription') : t('shareDescription') })] }), _jsxs(ModalBody, { className: "max-h-[60vh] space-y-4 overflow-y-auto", children: [_jsxs(Field, { className: "gap-1.5", children: [_jsx(FieldLabel, { children: t('whoCanUse') }), _jsx("ul", { className: "space-y-2", "data-testid": "share-audience", children: privateOwner ? (_jsx(AudienceRow, { avatar: _jsx(UserAvatar, { email: privateOwner.label, hint: privateOwner.label, size: "sm" }), label: privateOwner.label, meta: keepOwner ? t('ownerMeta') : t('removedMeta'), removed: !keepOwner, action: keepOwner ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-8 shrink-0", "aria-label": t('remove', { name: privateOwner.label }), disabled: save.isPending, onClick: () => setKeepOwner(false), children: _jsx(XIcon, { className: "size-3.5" }) })) : (_jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: save.isPending, onClick: () => setKeepOwner(true), children: t('keep') })) })) : current.length === 0 ? (_jsx(AudienceRow, { avatar: _jsx(EntityAvatar, { icon: UsersThreeIcon, label: everyoneLabel, hint: everyoneLabel, size: "sm" }), label: everyoneLabel, meta: t('everyoneDefaultMeta') })) : (current.map((grant) => {
180
181
  const isRemoved = removed.has(grant.grant_id);
181
182
  const label = grant.principal_type === 'project' ? everyoneLabel : grant.label;
182
- return (_jsx(AudienceRow, { avatar: grant.principal_type === 'member' ? (_jsx(UserAvatar, { email: grant.label, size: "sm" })) : (_jsx(EntityAvatar, { icon: grant.principal_type === 'group'
183
- ? UsersIcon
184
- : grant.principal_type === 'agent'
185
- ? RobotIcon
186
- : UsersThreeIcon, label: label, size: "sm" })), label: label, meta: isRemoved
183
+ return (_jsx(AudienceRow, { avatar: grant.principal_type === 'member' ? (_jsx(UserAvatar, { email: grant.label, hint: grant.label, size: "sm" })) : grant.principal_type === 'agent' ? (_jsx(AgentMark, { slug: grant.label })) : (_jsx(EntityAvatar, { icon: grant.principal_type === 'group' ? UsersIcon : UsersThreeIcon, label: label, hint: label, size: "sm" })), label: label, meta: isRemoved
187
184
  ? t('removedMeta')
188
185
  : grant.principal_type === 'group'
189
186
  ? t('groupMeta')
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { AgentMark } from '../../internal/components/agent-mark.js';
3
4
  import { useTranslations as useI18nTranslations } from '../../i18n/index.js';
4
5
  // AccessDialog — THE modal for every "give / edit access" interaction.
5
6
  //
@@ -909,7 +910,7 @@ function FixedPrincipals({ principals }) {
909
910
  const tI18nComplete = useI18nTranslations('hardcodedUi.i18nComplete');
910
911
  const tAgents = useI18nTranslations('agentPrincipals');
911
912
  return (_jsx("ul", { className: "space-y-2", children: principals.map((principal) => (_jsxs("li", { className: "bg-popover flex items-center gap-2.5 rounded-md border px-3 py-2", children: [principal.avatar ??
912
- (principal.type === 'group' ? (_jsx(EntityAvatar, { label: principal.label, size: "sm" })) : principal.type === 'agent' ? (_jsx(EntityAvatar, { icon: RobotIcon, label: principal.label, size: "sm" })) : (_jsx(UserAvatar, { email: principal.label, size: "sm" }))), _jsx("span", { className: "text-foreground min-w-0 flex-1 truncate text-sm font-medium", children: principal.label }), principal.type === 'group' ? (_jsx(Badge, { variant: "outline", size: "sm", children: tI18nComplete.raw('text34ca0e766088') })) : principal.type === 'agent' ? (_jsx(Badge, { variant: "outline", size: "sm", children: tAgents('agentBadge') })) : null] }, principal.id))) }));
913
+ (principal.type === 'group' ? (_jsx(EntityAvatar, { label: principal.label, hint: principal.label, size: "sm" })) : principal.type === 'agent' ? (_jsx(AgentMark, { slug: principal.label })) : (_jsx(UserAvatar, { email: principal.label, hint: principal.label, size: "sm" }))), _jsx("span", { className: "text-foreground min-w-0 flex-1 truncate text-sm font-medium", children: principal.label }), principal.type === 'group' ? (_jsx(Badge, { variant: "outline", size: "sm", children: tI18nComplete.raw('text34ca0e766088') })) : principal.type === 'agent' ? (_jsx(Badge, { variant: "outline", size: "sm", children: tAgents('agentBadge') })) : null] }, principal.id))) }));
913
914
  }
914
915
  function ScopeLine({ icon: Icon, label, items, }) {
915
916
  return (_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx(Icon, { className: "text-muted-foreground/70 size-3.5 shrink-0" }), _jsx("span", { className: "text-muted-foreground text-xs font-medium", children: label }), items.map((name) => (_jsx(Badge, { variant: "outline", size: "xs", className: "font-mono", children: name }, name)))] }));
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { AgentMark } from '../../internal/components/agent-mark.js';
3
4
  import { useTranslations } from '../../i18n/index.js';
4
5
  // PrincipalPicker — THE "who is this for?" control.
5
6
  //
@@ -25,7 +26,7 @@ import { useUiHostConfig, useUiViewer } from '../../providers/ui-provider.js';
25
26
  import { cn } from '../../internal/lib/utils.js';
26
27
  import { listAccountMembers, listGroups, listProjectAccess } from '@kortix/sdk';
27
28
  import { contract, qk } from '@kortix/sdk/react';
28
- import { CheckCircleIcon, MagnifyingGlassIcon, RobotIcon, UserPlusIcon, UsersIcon, UsersThreeIcon, } from '@phosphor-icons/react';
29
+ import { CheckCircleIcon, MagnifyingGlassIcon, UserPlusIcon, UsersIcon, UsersThreeIcon, } from '@phosphor-icons/react';
29
30
  import { useQuery } from '@tanstack/react-query';
30
31
  import { useMemo, useState } from 'react';
31
32
  import { principalLabel } from './access-shared.js';
@@ -219,7 +220,7 @@ export function PrincipalPicker({ scope, selection = 'multi', kinds = ['member',
219
220
  : selected.kind === 'member'
220
221
  ? principalLabel(rosterMembers.find((m) => m.user_id === selected.id)) || selected.id
221
222
  : selected.id;
222
- return (_jsxs("div", { className: cn('bg-popover flex items-center gap-2.5 rounded-md border px-3 py-2', className), children: [selected.kind === 'group' ? (_jsx(EntityAvatar, { icon: UsersIcon, label: label, size: "sm" })) : selected.kind === 'agent' ? (_jsx(EntityAvatar, { icon: RobotIcon, label: label, size: "sm" })) : (_jsx(UserAvatar, { email: label, size: "sm" })), _jsx("span", { className: "text-foreground min-w-0 flex-1 truncate text-sm font-medium", children: label }), _jsx(Badge, { variant: "outline", size: "sm", children: selected.kind === 'group'
223
+ return (_jsxs("div", { className: cn('bg-popover flex items-center gap-2.5 rounded-md border px-3 py-2', className), children: [selected.kind === 'group' ? (_jsx(EntityAvatar, { icon: UsersIcon, label: label, hint: label, size: "sm" })) : selected.kind === 'agent' ? (_jsx(AgentMark, { slug: label })) : (_jsx(UserAvatar, { email: label, hint: label, size: "sm" })), _jsx("span", { className: "text-foreground min-w-0 flex-1 truncate text-sm font-medium", children: label }), _jsx(Badge, { variant: "outline", size: "sm", children: selected.kind === 'group'
223
224
  ? 'Group'
224
225
  : selected.kind === 'invite'
225
226
  ? 'Invite'
@@ -258,10 +259,10 @@ export function PrincipalPicker({ scope, selection = 'multi', kinds = ['member',
258
259
  filteredMembers.length === 0 &&
259
260
  filteredAgents.length === 0 &&
260
261
  !inviteCandidate &&
261
- !showEveryone ? (_jsx("p", { className: "text-muted-foreground px-3 py-6 text-center text-xs", children: tI18nComplete.raw('texte8dd87902b91') })) : (_jsxs(_Fragment, { children: [showEveryone && everyone ? (_jsx(PickerRow, { selected: value.everyone === true, selection: selection, disabled: disabled, onSelect: () => pick({ kind: 'everyone', id: scope.kind === 'project' ? scope.projectId : '' }), leading: _jsx(EntityAvatar, { icon: UsersThreeIcon, label: everyone.label, size: "sm" }), label: everyone.label })) : null, inviteCandidate ? (_jsx(PickerRow, { selected: inviteSet.has(inviteCandidate), selection: selection, disabled: disabled, onSelect: () => pick({ kind: 'invite', id: inviteCandidate }), leading: _jsx("span", { className: "border-muted-foreground/40 text-muted-foreground flex size-6 shrink-0 items-center justify-center rounded-full border border-dashed", children: _jsx(UserPlusIcon, { className: "size-3.5" }) }), label: `Invite ${inviteCandidate}`, suffix: tI18nComplete.raw('text10d9e23d0185') })) : null, filteredGroups.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(PickerSectionLabel, { children: tI18nComplete.raw('text39bbb719fa2b') }), filteredGroups.map((group) => (_jsx(PickerRow, { selected: groupSet.has(group.group_id), selection: selection, disabled: disabled, onSelect: () => pick({ kind: 'group', id: group.group_id }), leading: _jsx(EntityAvatar, { icon: UsersIcon, label: group.name, size: "sm" }), label: group.name, suffix: tI18nComplete.raw('text24f15c9196e8') }, group.group_id)))] })) : null, filteredMembers.length > 0 ? (_jsxs(_Fragment, { children: [filteredGroups.length > 0 ? (_jsx(PickerSectionLabel, { children: tI18nComplete.raw('text1044a4c056d0') })) : null, filteredMembers.map((member) => {
262
+ !showEveryone ? (_jsx("p", { className: "text-muted-foreground px-3 py-6 text-center text-xs", children: tI18nComplete.raw('texte8dd87902b91') })) : (_jsxs(_Fragment, { children: [showEveryone && everyone ? (_jsx(PickerRow, { selected: value.everyone === true, selection: selection, disabled: disabled, onSelect: () => pick({ kind: 'everyone', id: scope.kind === 'project' ? scope.projectId : '' }), leading: _jsx(EntityAvatar, { icon: UsersThreeIcon, label: everyone.label, hint: everyone.label, size: "sm" }), label: everyone.label })) : null, inviteCandidate ? (_jsx(PickerRow, { selected: inviteSet.has(inviteCandidate), selection: selection, disabled: disabled, onSelect: () => pick({ kind: 'invite', id: inviteCandidate }), leading: _jsx("span", { className: "border-muted-foreground/40 text-muted-foreground flex size-6 shrink-0 items-center justify-center rounded-full border border-dashed", children: _jsx(UserPlusIcon, { className: "size-3.5" }) }), label: `Invite ${inviteCandidate}`, suffix: tI18nComplete.raw('text10d9e23d0185') })) : null, filteredGroups.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(PickerSectionLabel, { children: tI18nComplete.raw('text39bbb719fa2b') }), filteredGroups.map((group) => (_jsx(PickerRow, { selected: groupSet.has(group.group_id), selection: selection, disabled: disabled, onSelect: () => pick({ kind: 'group', id: group.group_id }), leading: _jsx(EntityAvatar, { icon: UsersIcon, label: group.name, hint: group.name, size: "sm" }), label: group.name, suffix: tI18nComplete.raw('text24f15c9196e8') }, group.group_id)))] })) : null, filteredMembers.length > 0 ? (_jsxs(_Fragment, { children: [filteredGroups.length > 0 ? (_jsx(PickerSectionLabel, { children: tI18nComplete.raw('text1044a4c056d0') })) : null, filteredMembers.map((member) => {
262
263
  const label = principalLabel(member) || member.user_id;
263
- return (_jsx(PickerRow, { selected: memberSet.has(member.user_id), selection: selection, disabled: disabled, onSelect: () => pick({ kind: 'member', id: member.user_id }), leading: _jsx(UserAvatar, { email: label, size: "sm", variant: "primary" }), label: label, suffix: member.user_id === viewerId ? '(you)' : undefined }, member.user_id));
264
- })] })) : null, filteredAgents.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(PickerSectionLabel, { children: tAgents('agentsSection') }), filteredAgents.map((agent) => (_jsx(PickerRow, { selected: agentSet.has(agent.service_account_id), selection: selection, disabled: disabled, onSelect: () => pick({ kind: 'agent', id: agent.service_account_id }), leading: _jsx(EntityAvatar, { icon: RobotIcon, label: agent.agent_name ?? '', size: "sm" }), label: agent.agent_name ?? agent.name, suffix: tAgents('agentBadge') }, agent.service_account_id)))] })) : null] })) })] }));
264
+ return (_jsx(PickerRow, { selected: memberSet.has(member.user_id), selection: selection, disabled: disabled, onSelect: () => pick({ kind: 'member', id: member.user_id }), leading: _jsx(UserAvatar, { email: label, hint: label, size: "sm", variant: "primary" }), label: label, suffix: member.user_id === viewerId ? '(you)' : undefined }, member.user_id));
265
+ })] })) : null, filteredAgents.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(PickerSectionLabel, { children: tAgents('agentsSection') }), filteredAgents.map((agent) => (_jsx(PickerRow, { selected: agentSet.has(agent.service_account_id), selection: selection, disabled: disabled, onSelect: () => pick({ kind: 'agent', id: agent.service_account_id }), leading: _jsx(AgentMark, { slug: agent.agent_name ?? agent.name }), label: agent.agent_name ?? agent.name, suffix: tAgents('agentBadge') }, agent.service_account_id)))] })) : null] })) })] }));
265
266
  }
266
267
  function PickerSectionLabel({ children }) {
267
268
  return (_jsx("p", { className: "text-muted-foreground px-2 pt-2 pb-1 text-xs font-medium first:pt-1.5", children: children }));
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from 'react';
2
+ /**
3
+ * An Agents-page card led by the agent's pixel-mark cover. The cover gives
4
+ * every agent a face that is the same wherever the agent appears, so a long
5
+ * list is scanned by shape and tone before names are read.
6
+ */
7
+ export declare function AgentCoverCard({ slug, href, onIntent, title, description, badges, meta, coverLabel, }: {
8
+ slug: string;
9
+ href: string;
10
+ onIntent?: () => void;
11
+ title: ReactNode;
12
+ description?: string | null;
13
+ badges?: ReactNode;
14
+ meta?: ReactNode;
15
+ /** A short pill on the cover's top-left corner — "Default". */
16
+ coverLabel?: string;
17
+ }): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { UiLink as Link } from '../../providers/ui-provider.js';
4
+ import { AgentMarkGlyph, agentMarkVars } from '../../internal/components/agent-mark.js';
5
+ import { cn } from '../../internal/lib/utils.js';
6
+ /**
7
+ * An Agents-page card led by the agent's pixel-mark cover. The cover gives
8
+ * every agent a face that is the same wherever the agent appears, so a long
9
+ * list is scanned by shape and tone before names are read.
10
+ */
11
+ export function AgentCoverCard({ slug, href, onIntent, title, description, badges, meta, coverLabel, }) {
12
+ return (_jsxs(Link, { href: href, prefetch: true, onPointerEnter: onIntent, onFocus: onIntent, style: agentMarkVars(slug), className: cn('group bg-card border-border/60 flex flex-col overflow-hidden rounded-md border text-left', 'hover:border-border transition-[border-color] duration-(--duration-normal) ease-out', 'focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none'), children: [_jsxs("span", { className: "relative flex h-31 items-center justify-center bg-(--mark-tile)", children: [_jsx(AgentMarkGlyph, { slug: slug, size: 56 }), coverLabel ? (_jsx("span", { className: "bg-background/20 absolute top-2.5 left-2.5 rounded-sm px-2 py-0.5 text-xs font-medium text-(--mark-ink) backdrop-blur-sm", children: coverLabel })) : null] }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-1 px-3.5 pt-3 pb-3.5", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "text-foreground truncate text-sm font-medium", children: title }), badges] }), description ? (_jsx("span", { className: "text-muted-foreground line-clamp-2 text-xs text-pretty", children: description })) : null, meta ? (_jsx("span", { className: "text-muted-foreground mt-auto flex flex-wrap items-center gap-x-2 gap-y-0.5 pt-1.5 text-xs", children: meta })) : null] })] }));
13
+ }
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { AgentMark } from '../../internal/components/agent-mark.js';
3
4
  import { EntityFilePane } from '../shared/entity/entity-modal.js';
4
5
  import { agentStatus } from './agent-status.js';
5
6
  import { useDefaultAgent } from './use-default-agent.js';
@@ -66,7 +67,7 @@ import { errorToast, successToast } from '../../primitives/toast.js';
66
67
  import { EmptyState } from '../../primitives/empty-state.js';
67
68
  import { ErrorState } from '../../primitives/error-state.js';
68
69
  import { configEntitySourcePath } from '../sections/component/config-entity-source-path.js';
69
- import { AgentConfigSections, agentConfigSectionKeys, agentEditorOptionQueries, DEFAULT_AGENT_CONFIG_SECTION, grantSummary, isAgentConfigSectionKey, registryGrantOptions, useAgentDraft, useAgentEditorOptions, useLocalizedAgentConfigCatalog, } from '../sections/view/agent-editor.js';
70
+ import { AgentConfigSections, agentConfigSectionKeys, agentEditorOptionQueries, DEFAULT_AGENT_CONFIG_SECTION, isAgentConfigSectionKey, registryGrantOptions, useAgentDraft, useAgentEditorOptions, useLocalizedAgentConfigCatalog, } from '../sections/view/agent-editor.js';
70
71
  import { formatMode, toArray } from '../shared/utils.js';
71
72
  import { editConfigPrompt, useConfigureThread, } from '../use-configure-thread.js';
72
73
  import { agentConfigNeedsUpgrade, useAgentConfig, useUpdateAgentConfig, } from '../../internal/hooks/projects/use-agent-config.js';
@@ -92,7 +93,6 @@ import { AgentDrivesSection } from './agent-drives-section.js';
92
93
  import { AgentAuthorityCard } from './agent-authority-card.js';
93
94
  import { AgentModel, AgentScope } from './agent-detail-aside.js';
94
95
  import { AppsGrantPage, ConnectorsGrantPage, SecretsGrantPage, SkillsGrantPage, } from './agent-grant-pages.js';
95
- import { GroupedUnderlineTabs, GroupSwitchTabs, InlineVerticalTabs, PrototypeSwitcher, useNavVariant, } from './agent-nav.prototype.js';
96
96
  import { AgentPeopleSection } from './agent-people-section.js';
97
97
  import { AgentShareControl } from './agent-share-control.js';
98
98
  import { AgentTriggersSection } from './agent-triggers-section.js';
@@ -164,7 +164,7 @@ export function AgentPage({ projectId, agentName }) {
164
164
  * the one scroller; the rail is the Settings rail
165
165
  * (`project-settings-page.tsx`), row for row.
166
166
  */
167
- function AgentPageFrame({ header, section, sections, sectionHref, pane, footer, summaries, }) {
167
+ function AgentPageFrame({ header, section, sections, sectionHref, pane, footer, }) {
168
168
  const tI18nComplete = useI18nTranslations('hardcodedUi.i18nComplete');
169
169
  const isMobile = useIsMobile();
170
170
  const localizedConfig = useLocalizedAgentConfigCatalog();
@@ -186,18 +186,7 @@ function AgentPageFrame({ header, section, sections, sectionHref, pane, footer,
186
186
  // The Settings rail's row dialect (`project-settings-page.tsx`).
187
187
  'gap-2 px-2.5 py-1 font-normal transition-none has-[>svg]:px-2.5', 'text-foreground data-[state=inactive]:text-foreground hover:bg-hover hover:text-foreground', 'data-[state=active]:bg-active data-[state=active]:font-medium', '[&_svg]:text-muted-foreground data-[state=active]:[&_svg]:text-foreground'), children: _jsxs(Link, { href: sectionHref(item.key), prefetch: true, children: [_jsx(SectionIcon, { className: "size-4 shrink-0" }), _jsx("span", { className: cn(!horizontal && 'truncate'), children: item.label })] }) }, item.key));
188
188
  };
189
- // PROTOTYPE: `?variant=` swaps the rail for one of the candidate navs.
190
- const variant = useNavVariant();
191
- const navProps = {
192
- groups,
193
- section,
194
- href: (key) => sectionHref(key),
195
- summaries,
196
- };
197
- if (!isMobile && variant !== 'rail') {
198
- return (_jsxs("div", { className: "relative flex h-full min-h-0 flex-1 flex-col", children: [_jsx("div", { className: cn('shrink-0 px-5 py-3', variant === 'inline' && 'border-border/60 border-b'), children: header }), variant === 'tabs' ? _jsx(GroupedUnderlineTabs, { ...navProps }) : null, variant === 'groups' ? _jsx(GroupSwitchTabs, { ...navProps }) : null, _jsx("main", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-y-auto", children: variant === 'inline' ? (_jsx(InlineVerticalTabs, { ...navProps, children: pane })) : (_jsx("div", { className: "mx-auto w-full max-w-2xl px-4 py-10 pb-32 lg:py-14 lg:pb-32", children: pane })) }), footer, _jsx(PrototypeSwitcher, {})] }));
199
- }
200
- return (_jsxs("div", { className: "relative flex h-full min-h-0 flex-1 flex-col", children: [_jsx("div", { className: "border-border/60 shrink-0 border-b px-5 py-3", children: header }), _jsxs("div", { className: cn('min-h-0 flex-1', isMobile ? 'flex flex-col overflow-hidden' : 'grid overflow-hidden'), style: isMobile ? undefined : { gridTemplateColumns: `${SETTINGS_SIDEBAR_WIDTH_PX}px 1fr` }, children: [isMobile ? (_jsx("nav", { "aria-label": tI18nComplete.raw('text7be318295927'), className: "border-border/60 flex h-auto shrink-0 items-center border-b", children: _jsx(FadedScrollArea, { orientation: "horizontal", fadeColor: "from-background", className: "min-w-0 flex-1 py-2", children: _jsx(Tabs, { value: section, className: "w-fit", children: _jsx(TabsList, { orientation: "horizontal", className: "w-fit", children: items.map((item) => trigger(item, true)) }) }) }) })) : (_jsx("aside", { className: "flex min-h-0 flex-col border-r bg-inherit", children: _jsx("nav", { "aria-label": tI18nComplete.raw('text7be318295927'), className: "flex min-h-0 flex-1 [scrollbar-width:none] flex-col gap-4 overflow-y-auto px-2 pt-3 pb-2 [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden", children: _jsx(Tabs, { value: section, orientation: "vertical", className: "space-y-3", children: groups.map((g) => (_jsxs("div", { children: [showGroupLabels ? (_jsx("div", { className: "text-muted-foreground flex h-7 items-center px-2.5 text-xs font-medium", children: g.group })) : null, _jsx(TabsList, { orientation: "vertical", className: "w-full", children: g.items.map((item) => trigger(item, false)) })] }, g.group))) }) }) })), _jsx("main", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-y-auto", children: _jsx("div", { className: "mx-auto w-full max-w-2xl px-4 py-10 pb-32 lg:py-14 lg:pb-32", children: pane }) })] }), footer, _jsx(PrototypeSwitcher, {})] }));
189
+ return (_jsxs("div", { className: "relative flex h-full min-h-0 flex-1 flex-col", children: [_jsx("div", { className: "border-border/60 shrink-0 border-b px-5 py-3", children: header }), _jsxs("div", { className: cn('min-h-0 flex-1', isMobile ? 'flex flex-col overflow-hidden' : 'grid overflow-hidden'), style: isMobile ? undefined : { gridTemplateColumns: `${SETTINGS_SIDEBAR_WIDTH_PX}px 1fr` }, children: [isMobile ? (_jsx("nav", { "aria-label": tI18nComplete.raw('text7be318295927'), className: "border-border/60 flex h-auto shrink-0 items-center border-b", children: _jsx(FadedScrollArea, { orientation: "horizontal", fadeColor: "from-background", className: "min-w-0 flex-1 py-2", children: _jsx(Tabs, { value: section, className: "w-fit", children: _jsx(TabsList, { orientation: "horizontal", className: "w-fit", children: items.map((item) => trigger(item, true)) }) }) }) })) : (_jsx("aside", { className: "flex min-h-0 flex-col border-r bg-inherit", children: _jsx("nav", { "aria-label": tI18nComplete.raw('text7be318295927'), className: "flex min-h-0 flex-1 [scrollbar-width:none] flex-col gap-4 overflow-y-auto px-2 pt-3 pb-2 [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden", children: _jsx(Tabs, { value: section, orientation: "vertical", className: "space-y-3", children: groups.map((g) => (_jsxs("div", { children: [showGroupLabels ? (_jsx("div", { className: "text-muted-foreground flex h-7 items-center px-2.5 text-xs font-medium", children: g.group })) : null, _jsx(TabsList, { orientation: "vertical", className: "w-full", children: g.items.map((item) => trigger(item, false)) })] }, g.group))) }) }) })), _jsx("main", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-y-auto", children: _jsx("div", { className: "mx-auto w-full max-w-2xl px-4 py-10 pb-32 lg:py-14 lg:pb-32", children: pane }) })] }), footer] }));
201
190
  }
202
191
  function CenteredState({ children }) {
203
192
  return (_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: _jsx("div", { className: "mx-auto w-full max-w-3xl px-6 py-16", children: children }) }));
@@ -270,7 +259,7 @@ function AgentChips({ agent, config, size, }) {
270
259
  */
271
260
  function AgentHeader({ projectId, agent, config, canWrite, children, }) {
272
261
  const tI18nComplete = useI18nTranslations('hardcodedUi.i18nComplete');
273
- return (_jsxs("header", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("nav", { "aria-label": tI18nComplete.raw('text2bd873d6c734'), className: "flex min-w-0 items-center gap-1.5 text-sm", children: [_jsx(Link, { href: capabilityTabHref(projectId, 'agent'), prefetch: true, className: "text-muted-foreground hover:text-foreground shrink-0 transition-colors", children: tI18nComplete.raw('text279b44d2ab4b') }), _jsx(CaretRightIcon, { "aria-hidden": true, className: "text-muted-foreground size-3.5 shrink-0" }), _jsx("h1", { className: "text-foreground truncate text-sm font-semibold", children: isMetaAgentName(agent.name) ? META_AGENT_DISPLAY_NAME : capitalizeWords(agent.name) }), _jsx(AgentChips, { agent: agent, config: config, size: "xs" })] }), _jsx(AgentActions, { projectId: projectId, agent: agent, config: config, canWrite: canWrite })] }), children] }));
262
+ return (_jsxs("header", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("nav", { "aria-label": tI18nComplete.raw('text2bd873d6c734'), className: "flex min-w-0 items-center gap-1.5 text-sm", children: [_jsx(Link, { href: capabilityTabHref(projectId, 'agent'), prefetch: true, className: "text-muted-foreground hover:text-foreground shrink-0 transition-colors", children: tI18nComplete.raw('text279b44d2ab4b') }), _jsx(CaretRightIcon, { "aria-hidden": true, className: "text-muted-foreground size-3.5 shrink-0" }), _jsx(AgentMark, { slug: agent.name }), _jsx("h1", { className: "text-foreground truncate text-sm font-semibold", children: isMetaAgentName(agent.name) ? META_AGENT_DISPLAY_NAME : capitalizeWords(agent.name) }), _jsx(AgentChips, { agent: agent, config: config, size: "xs" })] }), _jsx(AgentActions, { projectId: projectId, agent: agent, config: config, canWrite: canWrite })] }), children] }));
274
263
  }
275
264
  // ─── Editable body ─────────────────────────────────────────────────────────
276
265
  /** What a v1 project, or a reader without write, can still see. */
@@ -302,22 +291,6 @@ function EditableAgentPage({ projectId, agent, config, initial, schemaVersion, }
302
291
  const sections = useMemo(() => agentConfigSectionKeys(schemaVersion).filter((k) => drivesOn || k !== 'drives'), [schemaVersion, drivesOn]);
303
292
  const registryOptions = useMemo(() => registryGrantOptions({ manifest_raw: config.manifest_raw, agents: toArray(config.agents) }, agent.name), [config.manifest_raw, config.agents, agent.name]);
304
293
  const section = useAgentSection(sections);
305
- // PROTOTYPE: the `inline` nav variant shows each topic's current value.
306
- const summaries = useMemo(() => {
307
- const d = editor.draft;
308
- const grant = (v) => grantSummary(v, tI18nComplete).label;
309
- return {
310
- skills: grant(d.skills),
311
- connectors: grant(d.connectors),
312
- secrets: grant(d.secrets),
313
- apps: grant(d.apps),
314
- actions: grant(d.kortix_permissions ?? d.kortix_cli),
315
- subagents: grant(d.subagents),
316
- commands: grant(d.commands),
317
- extensions: grant(d.extensions),
318
- model: editor.oc.model ? (editor.oc.model.split('/').pop() ?? editor.oc.model) : 'Default',
319
- };
320
- }, [editor.draft, editor.oc.model, tI18nComplete]);
321
294
  const onSave = useCallback(async () => {
322
295
  if (!editor.isDirty || update.isPending)
323
296
  return;
@@ -335,7 +308,7 @@ function EditableAgentPage({ projectId, agent, config, initial, schemaVersion, }
335
308
  // shared across topics and nothing is lost.
336
309
  const leaveGuard = useUnsavedChangesGuard(editor.isDirty, pathname);
337
310
  useSaveShortcut(onSave);
338
- return (_jsx(AgentPageFrame, { header: _jsx(AgentHeader, { projectId: projectId, agent: agent, config: config, canWrite: true }), section: section, sections: sections, sectionHref: sectionHrefFor(pathname), summaries: summaries, pane: _jsx(AgentConfigSections, { section: section, editor: editor, options: options, skillsOptions: skillsOptions, registryOptions: registryOptions, skills: _jsx(SkillsGrantPage, { projectId: projectId, config: config, editor: editor }), connectors: _jsx(ConnectorsGrantPage, { projectId: projectId, editor: editor }), secrets: _jsx(SecretsGrantPage, { projectId: projectId, editor: editor }), apps: _jsx(AppsGrantPage, { projectId: projectId, editor: editor }), drives: _jsx(AgentDrivesSection, { projectId: projectId, agentName: agent.name }), overview: _jsx(OverviewPane, { description: editor.oc.description ?? '', onDescriptionChange: (next) => editor.setOc('description', next), descriptionHelp: editor.oc.mode === 'subagent'
311
+ return (_jsx(AgentPageFrame, { header: _jsx(AgentHeader, { projectId: projectId, agent: agent, config: config, canWrite: true }), section: section, sections: sections, sectionHref: sectionHrefFor(pathname), pane: _jsx(AgentConfigSections, { section: section, editor: editor, options: options, skillsOptions: skillsOptions, registryOptions: registryOptions, skills: _jsx(SkillsGrantPage, { projectId: projectId, config: config, editor: editor }), connectors: _jsx(ConnectorsGrantPage, { projectId: projectId, editor: editor }), secrets: _jsx(SecretsGrantPage, { projectId: projectId, editor: editor }), apps: _jsx(AppsGrantPage, { projectId: projectId, editor: editor }), drives: _jsx(AgentDrivesSection, { projectId: projectId, agentName: agent.name }), overview: _jsx(OverviewPane, { description: editor.oc.description ?? '', onDescriptionChange: (next) => editor.setOc('description', next), descriptionHelp: editor.oc.mode === 'subagent'
339
312
  ? 'Required. This is how other agents decide to call it.'
340
313
  : 'One line on what this agent is for. Other agents read it when picking a subagent.', prompt: editor.oc.prompt ?? '', onPromptChange: (next) => editor.setOc('prompt', next) }), triggers: _jsx(AgentTriggersSection, { projectId: projectId, agentName: agent.name, defaultAgent: config.default_agent ?? config.open_code_default_agent }), people: _jsx(AgentPeopleSection, { projectId: projectId, agentName: agent.name }), authority: _jsx(AgentAuthorityCard, { projectId: projectId, agentName: agent.name, grant: editor.draft.kortix_permissions ?? editor.draft.kortix_cli }) }), footer: _jsxs(_Fragment, { children: [_jsx(SaveBar, { dirty: editor.isDirty, pending: update.isPending, onDiscard: () => setConfirmDiscard(true), onSave: onSave }), _jsx(ConfirmDialog, { open: confirmDiscard, onOpenChange: setConfirmDiscard, title: tI18nComplete.raw('text3b13192b9d88'), description: tI18nComplete('text8a36cce34f68', { value0: capitalizeWords(agent.name) }), confirmLabel: tI18nComplete.raw('texteb1a70e39274'), cancelLabel: tI18nComplete.raw('texte76fd2add010'), confirmVariant: "destructive", onConfirm: () => {
341
314
  setConfirmDiscard(false);
@@ -16,22 +16,22 @@ import { detectManifestVersion } from '../../internal/lib/manifest-version.js';
16
16
  import { toArray } from '../shared/utils.js';
17
17
  import { newConfigPrompt, useConfigureThread, } from '../use-configure-thread.js';
18
18
  import { PROJECT_ACTIONS } from '../../internal/lib/project-actions.js';
19
- import { Skeleton } from '../../primitives/skeleton.js';
20
19
  import { useProjectCan } from '../../internal/lib/use-project-can.js';
21
20
  import { cn } from '../../internal/lib/utils.js';
22
21
  import { getProjectDetail, listProjectResourceGrants, listProjectTriggers, updateProjectDefaultAgent, } from '@kortix/sdk';
23
22
  import { contract, qk, useProjectAccountId } from '@kortix/sdk/react';
24
23
  import { capitalizeWords, isMetaAgentName, META_AGENT_DISPLAY_NAME } from '../../_vendor/kortix-shared.js';
25
- import { CaretRightIcon, MagnifyingGlassIcon, RobotIcon, StarIcon as StarSolid, } from '@phosphor-icons/react';
24
+ import { MagnifyingGlassIcon, RobotIcon } from '@phosphor-icons/react';
26
25
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
27
26
  import { CapabilityPageShell } from '../shared/capability-page-shell.js';
28
27
  import { agentHref } from '../../internal/lib/capability-tab-routes.js';
29
28
  import { prefetchAgentConfig } from '../../internal/hooks/projects/use-agent-config.js';
30
- import { CatalogCard } from '../shared/catalog/catalog-card.js';
31
29
  import { catalogEmptyKind } from '../shared/catalog/catalog-empty.js';
32
30
  import { CatalogNoMatch } from '../shared/catalog/catalog-empty-state.js';
33
31
  import { CatalogGrid } from '../shared/catalog/catalog-grid.js';
34
- import { DENSE_GRID_CLASSNAME, DENSE_GRID_CONTAINER_CLASSNAME, } from '../shared/catalog/catalog-grid-tokens.js';
32
+ import { GRID_CLASSNAME } from '../shared/catalog/catalog-grid-tokens.js';
33
+ import { AgentMark } from '../../internal/components/agent-mark.js';
34
+ import { AgentCoverCard } from './agent-cover-card.js';
35
35
  import { agentInMode, filterAgents } from './agent-filter.js';
36
36
  import { useSlashFocus } from '../../internal/hooks/index.js';
37
37
  /**
@@ -92,8 +92,8 @@ export function AgentsPage({ projectId }) {
92
92
  }
93
93
  return counts;
94
94
  }, [triggersQuery.data, config]);
95
- // Who is granted each agent — the card's "N people" fact. Same key the
96
- // agent page's People section reads, manager-gated like it.
95
+ // Who is granted each agent. Same key the agent page's People section
96
+ // reads, manager-gated like it.
97
97
  const grantsQuery = useQuery({
98
98
  queryKey: qk.project.resourceGrants(projectId),
99
99
  queryFn: () => listProjectResourceGrants(projectId),
@@ -110,6 +110,7 @@ export function AgentsPage({ projectId }) {
110
110
  }
111
111
  return counts;
112
112
  }, [grantsQuery.data]);
113
+ const facts = (agent) => (_jsx(AgentCardFacts, { agent: agent, triggerCount: triggerCounts.get(agent.name) ?? 0, grantCount: grantCounts.get(agent.name) ?? 0 }));
113
114
  const agents = useMemo(() => toArray(config?.agents), [config]);
114
115
  const filtered = useMemo(() => filterAgents(agents, { mode: null, query }), [agents, query]);
115
116
  const defaultAgent = config?.default_agent ?? config?.open_code_default_agent ?? null;
@@ -122,8 +123,6 @@ export function AgentsPage({ projectId }) {
122
123
  .filter((agent) => agentInMode(agent, 'primary'))
123
124
  .sort((a, b) => Number(b.name === defaultAgent) - Number(a.name === defaultAgent)), [filtered, defaultAgent]);
124
125
  const subagents = useMemo(() => filtered.filter((agent) => !agentInMode(agent, 'primary')), [filtered]);
125
- const subagentTotal = agents.filter((agent) => !agentInMode(agent, 'primary')).length;
126
- const primaryTotal = agents.length - subagentTotal;
127
126
  // `null` = render the grid. Otherwise which "nothing to show" copy applies:
128
127
  // genuinely zero agents vs. agents exist but this filter/search hid all of
129
128
  // them. Telling the user "No agents yet" in the second case is false and
@@ -133,69 +132,64 @@ export function AgentsPage({ projectId }) {
133
132
  // title beside it and can be terse; the empty state is the whole screen and
134
133
  // has to name what it creates. Both start the same configure thread, so they
135
134
  // cannot drift apart.
136
- // `size="sm"` is `h-8` — the same height as the search input beside it in the
137
- // header group. The Button default is `h-9`, which left the pair 4px
138
- // mismatched on a row that is centred, so both edges were off.
139
- const createButton = (label) => canWrite ? (_jsx(NewEntityMenu, { label: label, pending: configure.pending, onChat: () => configure.start(newConfigPrompt('agent')), manual: {
135
+ // The header button is `h-9`, the height of the `size="sm"` search input
136
+ // beside it. The empty state keeps `sm` to match its `sm` docs link.
137
+ const createButton = (label, size) => canWrite ? (_jsx(NewEntityMenu, { label: label, size: size, pending: configure.pending, onChat: () => configure.start(newConfigPrompt('agent')), manual: {
140
138
  description: tI18nComplete.raw('text5210c5acc2cd'),
141
139
  href: uiRoutes().files(projectId) ?? customizePath(projectId, 'repo'),
142
140
  } })) : null;
143
- return (_jsxs(CapabilityPageShell, { title: tI18nComplete.raw('text279b44d2ab4b'), description: tI18nComplete.raw('text636453e23d69'), action: createButton('New'), search: _jsxs(InputGroupSearch, { children: [_jsx(InputGroupSearchIcon, { children: _jsx(MagnifyingGlassIcon, {}) }), _jsx(InputGroupSearchInput, { ref: searchRef, placeholder: tI18nComplete.raw('text212fd04a1d20'), value: query, onChange: (event) => setQuery(event.target.value), variant: "popover", size: "sm" }), _jsx(InputGroupSearchClear, { onClick: () => setQuery('') })] }), filters:
144
- // The row holds its height while the manifest loads, so the grid
145
- // below does not drop when the count and the selector land.
146
- detailQuery.isLoading ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { className: "h-4 w-40 rounded-sm py-0" }), _jsx(Skeleton, { className: "h-8 w-44 rounded-md py-0" })] })) : config ? (_jsxs(_Fragment, { children: [_jsxs("p", { className: "text-muted-foreground text-xs", children: [primaryTotal, " ", primaryTotal === 1 ? 'agent' : 'agents', subagentTotal > 0
147
- ? ` · ${subagentTotal} ${subagentTotal === 1 ? 'subagent' : 'subagents'}`
148
- : null, ' ', tI18nComplete.raw('text582967534d0f'), ' ', _jsx("span", { className: "font-mono", children: tI18nComplete.raw('text1965f383021e') })] }), _jsx(DefaultAgentSelector, { projectId: projectId, config: config, canWrite: canWrite })] })) : undefined, children: [_jsx(CatalogGrid, { isLoading: detailQuery.isLoading, isError: detailQuery.isError, error: detailQuery.error, onRetry: () => detailQuery.refetch(), isEmpty: emptyKind !== null, empty: emptyKind === 'no-match' ? (_jsx(CatalogNoMatch, { query: query })) : (_jsx(EmptyState, { icon: RobotIcon, size: "sm", title: tI18nComplete.raw('text971b61f8cb4d'), description: tI18nComplete.raw('textc4e4294c6777'), action: createButton('Create an agent'), secondaryAction: _jsx(Button, { asChild: true, variant: "ghost", size: "sm", className: "gap-1.5", children: _jsx("a", { href: "https://opencode.ai/docs/agents/", target: "_blank", rel: "noopener noreferrer", children: tI18nComplete.raw('text7af023c43013') }) }) })), children: primaryAgents.map((agent) => (_jsx(CatalogCard, { href: agentHref(projectId, agent.name), onIntent: () => prefetchAgentConfig(queryClient, projectId, agent.name), title: agentTitle(agent.name), subtitle: _jsx("code", { className: "text-muted-foreground text-xs break-all", children: agent.name }), description: agent.description, badges: _jsx(AgentCardBadges, { agent: agent, isDefault: defaultAgent === agent.name }), meta: _jsx(AgentCardFacts, { agent: agent, triggerCount: triggerCounts.get(agent.name) ?? 0, peopleCount: canManageMembers ? (grantCounts.get(agent.name) ?? 0) : null }), trailing: _jsx(CardCaret, {}) }, agent.path))) }), !detailQuery.isLoading && !detailQuery.isError && subagents.length > 0 ? (_jsxs("section", { "aria-labelledby": "subagents-heading", className: cn('space-y-2', primaryAgents.length > 0 && 'mt-8'), children: [_jsxs("div", { children: [_jsxs("h2", { id: "subagents-heading", className: "text-foreground text-sm font-medium", children: [tI18nComplete.raw('agentsListSubagentsTitle'), ' ', _jsx("span", { className: "text-muted-foreground font-normal tabular-nums", children: subagents.length })] }), _jsx("p", { className: "text-muted-foreground mt-1 text-xs", children: tI18nComplete.raw('agentsListSubagentsHint') })] }), _jsx("div", { className: cn(DENSE_GRID_CONTAINER_CLASSNAME, '-mx-3'), children: _jsx("div", { className: DENSE_GRID_CLASSNAME, children: subagents.map((agent) => (_jsx(CatalogCard, { variant: "plain", href: agentHref(projectId, agent.name), onIntent: () => prefetchAgentConfig(queryClient, projectId, agent.name), title: agentTitle(agent.name), subtitle: _jsx("code", { className: "text-muted-foreground text-xs break-all", children: agent.name }), description: agent.description, badges: _jsx(AgentCardBadges, { agent: agent, isDefault: false }), meta: _jsx("span", { className: cn('truncate', agent.model && 'font-mono'), children: agent.model?.split('/').pop() ?? tI18nComplete.raw('text3840d9d29421') }), trailing: _jsx(CardCaret, {}) }, agent.path))) }) })] })) : null] }));
141
+ return (_jsxs(CapabilityPageShell, { title: tI18nComplete.raw('text279b44d2ab4b'), description: tI18nComplete.raw('text636453e23d69'), action:
142
+ // The default-agent selector sits in the header beside New: it is a
143
+ // page-level setting, not a filter on the list.
144
+ _jsxs(_Fragment, { children: [config ? (_jsx(DefaultAgentSelector, { projectId: projectId, config: config, canWrite: canWrite })) : null, createButton('New')] }), search: _jsxs(InputGroupSearch, { children: [_jsx(InputGroupSearchIcon, { children: _jsx(MagnifyingGlassIcon, {}) }), _jsx(InputGroupSearchInput, { ref: searchRef, placeholder: tI18nComplete.raw('text212fd04a1d20'), value: query, onChange: (event) => setQuery(event.target.value), variant: "popover", size: "sm" }), _jsx(InputGroupSearchClear, { onClick: () => setQuery('') })] }), children: [_jsx(CatalogGrid, { isLoading: detailQuery.isLoading, isError: detailQuery.isError, error: detailQuery.error, onRetry: () => detailQuery.refetch(), isEmpty: emptyKind !== null, empty: emptyKind === 'no-match' ? (_jsx(CatalogNoMatch, { query: query })) : (_jsx(EmptyState, { icon: RobotIcon, size: "sm", title: tI18nComplete.raw('text971b61f8cb4d'), description: tI18nComplete.raw('textc4e4294c6777'), action: createButton('Create an agent', 'sm'), secondaryAction: _jsx(Button, { asChild: true, variant: "ghost", size: "sm", className: "gap-1.5", children: _jsx("a", { href: "/docs/project/agents", target: "_blank", rel: "noopener noreferrer", children: tI18nComplete.raw('text7af023c43013') }) }) })), children: primaryAgents.map((agent) => (_jsx(AgentCoverCard, { slug: agent.name, href: agentHref(projectId, agent.name), onIntent: () => prefetchAgentConfig(queryClient, projectId, agent.name), title: agentTitle(agent.name), description: agent.description, badges: _jsx(AgentCardBadges, { agent: agent }), coverLabel: defaultAgent === agent.name ? tI18nComplete.raw('text21b111cbfe6e') : undefined, meta: facts(agent) }, agent.path))) }), !detailQuery.isLoading && !detailQuery.isError && subagents.length > 0 ? (_jsxs("section", { "aria-labelledby": "subagents-heading", className: cn('space-y-2', primaryAgents.length > 0 && 'mt-8'), children: [_jsxs("div", { children: [_jsxs("h2", { id: "subagents-heading", className: "text-foreground text-sm font-medium", children: [tI18nComplete.raw('agentsListSubagentsTitle'), ' ', _jsx("span", { className: "text-muted-foreground font-normal tabular-nums", children: subagents.length })] }), _jsx("p", { className: "text-muted-foreground mt-1 text-xs", children: tI18nComplete.raw('agentsListSubagentsHint') })] }), _jsx("div", { className: GRID_CLASSNAME, children: subagents.map((agent) => (_jsx(AgentCoverCard, { slug: agent.name, href: agentHref(projectId, agent.name), onIntent: () => prefetchAgentConfig(queryClient, projectId, agent.name), title: agentTitle(agent.name), description: agent.description, badges: _jsx(AgentCardBadges, { agent: agent }), meta: facts(agent) }, agent.path))) })] })) : null] }));
149
145
  }
150
146
  function agentTitle(name) {
151
147
  return isMetaAgentName(name) ? META_AGENT_DISPLAY_NAME : capitalizeWords(name);
152
148
  }
153
- function CardCaret() {
154
- return (_jsx(CaretRightIcon, { "aria-hidden": true, className: "text-muted-foreground group-hover:text-foreground mt-0.5 size-4 transition-colors" }));
155
- }
156
149
  /**
157
- * The card's facts line: what the agent runs on, what starts it, and who may
158
- * use it — the three things a person configuring access wants to know before
159
- * opening the page. Model falls back to the project default rather than
160
- * printing nothing; triggers and people print their count, including zero,
161
- * so every card has the same three slots and the eye can compare down a
162
- * column. People is omitted for a reader who cannot see grants.
150
+ * The access fact (KRTX-3025): "Full access", or the connectors an agent can
151
+ * reach. Nothing for an agent without connectors — the card does not list what
152
+ * an agent lacks.
163
153
  */
164
- function AgentCardFacts({ agent, triggerCount, peopleCount, }) {
165
- const tI18nComplete = useI18nTranslations('hardcodedUi.i18nComplete');
166
- const model = agent.model ? agent.model.split('/').pop() : null;
167
- const accessLabel = accessLabelOf(agentAccess(agent.scope), tI18nComplete);
168
- const sep = (_jsx("span", { "aria-hidden": true, className: "text-muted-foreground", children: "\u00B7" }));
169
- return (_jsxs(_Fragment, { children: [_jsx("span", { className: cn('truncate', model && 'font-mono'), children: model ?? tI18nComplete.raw('text3840d9d29421') }), sep, _jsxs("span", { className: "tabular-nums", children: [triggerCount, " ", triggerCount === 1 ? 'trigger' : 'triggers'] }), peopleCount !== null ? (_jsxs(_Fragment, { children: [sep, _jsx("span", { className: "tabular-nums", children: peopleCount === 0
170
- ? tI18nComplete.raw('textb6c70926c1aa')
171
- : `${peopleCount} ${peopleCount === 1 ? 'grant' : 'grants'}` })] })) : null, accessLabel ? _jsx("span", { className: "basis-full tabular-nums", children: accessLabel }) : null] }));
172
- }
173
- /** The access slot: what the configuration grants (KRTX-3025). */
174
154
  function accessLabelOf(access, t) {
175
155
  if (!access)
176
156
  return null;
177
157
  if (access.kind === 'full')
178
158
  return t.raw('textf19611c61ca5');
179
- const LABELS = {
180
- connectors: { all: 'textd83250185d41', count: 'text15589a542412' },
181
- secrets: { all: 'text0b38e3daeb93', count: 'texte96d99ad9896' },
182
- actions: { all: 'text70b8d7e10749', count: 'text2d3f4433505a' },
183
- };
184
- return access.parts
185
- .map(({ kind, value }) => value === 'all' ? t.raw(LABELS[kind].all) : t(LABELS[kind].count, { count: value }))
186
- .join(', ');
159
+ const connectors = access.parts.find((part) => part.kind === 'connectors')?.value ?? 0;
160
+ if (connectors === 'all')
161
+ return t.raw('textd83250185d41');
162
+ return connectors > 0 ? t('text15589a542412', { count: connectors }) : null;
163
+ }
164
+ /**
165
+ * The card's facts line, joined by `•`: access, the agent's own model,
166
+ * triggers, grants. A fact with nothing to say is left out — no "0 triggers",
167
+ * no "Default model" — so an empty slot never reads as information.
168
+ */
169
+ function AgentCardFacts({ agent, triggerCount, grantCount, }) {
170
+ const tI18nComplete = useI18nTranslations('hardcodedUi.i18nComplete');
171
+ const facts = [
172
+ accessLabelOf(agentAccess(agent.scope), tI18nComplete),
173
+ agent.model ? agent.model.split('/').pop() : null,
174
+ triggerCount > 0 ? `${triggerCount} ${triggerCount === 1 ? 'trigger' : 'triggers'}` : null,
175
+ grantCount > 0 ? `${grantCount} ${grantCount === 1 ? 'grant' : 'grants'}` : null,
176
+ ].filter((fact) => Boolean(fact));
177
+ if (facts.length === 0)
178
+ return null;
179
+ return (_jsx("span", { className: "truncate tabular-nums", children: facts.map((fact, index) => (_jsxs("span", { children: [index > 0 ? (_jsx("span", { "aria-hidden": true, className: "px-1.5", children: "\u2022" })) : null, fact] }, fact))) }));
187
180
  }
188
181
  /**
189
- * Card badges — "also a subagent", the default-agent star, disabled.
182
+ * Card badges — "also a subagent", disabled. The default agent is marked on
183
+ * the cover instead.
190
184
  *
191
185
  * The section already says primary or subagent, so a mode chip only marks the
192
186
  * both-ways `all` agent: it sits with the primaries but other agents call it
193
187
  * too.
194
188
  */
195
- function AgentCardBadges({ agent, isDefault }) {
189
+ function AgentCardBadges({ agent }) {
196
190
  const tI18nComplete = useI18nTranslations('hardcodedUi.i18nComplete');
197
191
  const mode = agent.mode?.toLowerCase();
198
- return (_jsxs(_Fragment, { children: [mode === 'all' ? (_jsx(Badge, { variant: "muted", size: "xs", children: tI18nComplete.raw('agentsListAlsoSubagent') })) : null, isDefault ? (_jsx(StarSolid, { weight: "fill", "aria-label": tI18nComplete.raw('text94da52ecd6c5'), className: "text-kortix-orange size-3.5 shrink-0" })) : null, agent.enabled === false ? (_jsx(Badge, { variant: "muted", size: "xs", children: tI18nComplete.raw('text75081b593d15') })) : null] }));
192
+ return (_jsxs(_Fragment, { children: [mode === 'all' ? (_jsx(Badge, { variant: "muted", size: "xs", children: tI18nComplete.raw('agentsListAlsoSubagent') })) : null, agent.enabled === false ? (_jsx(Badge, { variant: "muted", size: "xs", children: tI18nComplete.raw('text75081b593d15') })) : null] }));
199
193
  }
200
194
  /**
201
195
  * Which agent a new chat in this project starts with. Rides the filter row
@@ -227,5 +221,5 @@ function DefaultAgentSelector({ projectId, config, canWrite, }) {
227
221
  });
228
222
  if (!declaresDefaultAgent || availableAgents.length === 0 || !current)
229
223
  return null;
230
- return (_jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [_jsx("span", { className: "text-muted-foreground hidden text-xs sm:block", children: tI18nComplete.raw('text21b111cbfe6e') }), mutation.isPending ? _jsx(Loading, { className: "size-4 shrink-0" }) : null, _jsxs(Select, { value: current, onValueChange: (agentName) => mutation.mutate(agentName), disabled: !canWrite || mutation.isPending, children: [_jsx(SelectTrigger, { "aria-label": tI18nComplete.raw('text94da52ecd6c5'), className: "w-44 shrink-0", size: "sm", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { align: "end", children: availableAgents.map((agent) => (_jsx(SelectItem, { value: agent.name, children: isMetaAgentName(agent.name) ? META_AGENT_DISPLAY_NAME : capitalizeWords(agent.name) }, agent.name))) })] })] }));
224
+ return (_jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [mutation.isPending ? _jsx(Loading, { className: "size-4 shrink-0" }) : null, _jsxs(Select, { value: current, onValueChange: (agentName) => mutation.mutate(agentName), disabled: !canWrite || mutation.isPending, children: [_jsx(SelectTrigger, { "aria-label": tI18nComplete.raw('text94da52ecd6c5'), className: "h-9 w-44 shrink-0", size: "sm", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { align: "end", children: availableAgents.map((agent) => (_jsx(SelectItem, { value: agent.name, children: _jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx(AgentMark, { slug: agent.name }), _jsx("span", { className: "truncate", children: agentTitle(agent.name) })] }) }, agent.name))) })] })] }));
231
225
  }
@@ -24,5 +24,6 @@
24
24
  * Kept as a plain exported constant (not inlined at the call site) so it's
25
25
  * independently testable and diffable — this is the load-bearing artifact of
26
26
  * the feature, not the wiring around it.
27
+ * @deprecated Moves a v1 project to the OpenCode-era v2 layout (harnesses/opencode). OpenCode is sunset (2026-10-23); migrate with `kortix migrate` to kortix_version 3.
27
28
  */
28
29
  export declare const MIGRATE_TO_V2_PROMPT = "Migrate this project's manifest from kortix_version 1 (kortix.toml) to kortix_version 2 (kortix.yaml), and move its files to the root layout. Read everything first, then make the change, then land it as a change request \u2014 do not merge it yourself.\n\n## 1. Read before you write\n\n- The current manifest: `kortix.toml` (or `kortix.yaml` if this project already partially moved \u2014 check `kortix_version` at the top either way).\n- Any `[[agents]]` entries in the v1 manifest \u2014 these carry `connectors`, `kortix_permissions` (older manifests spell it `kortix_cli`), and `env` grants per agent name. An agent name with NO `[[agents]]` entry at all is today unrestricted (v1's back-compat default is \"all\" when a grant key is omitted).\n- `.kortix/opencode/opencode.jsonc` \u2014 if it sets a top-level `default_agent`, that is the project's existing default; use it. If it doesn't, pick the agent whose `.kortix/opencode/agents/*.md` frontmatter has `mode: primary` and reads as the general/primary one (usually the first-created or the one with the broadest permissions). Record which you picked and why in the change request description \u2014 a human reviews this before it merges, so a defensible choice beats blocking on it.\n- **You do NOT need to read each agent's `.md` frontmatter to migrate it.** v1's frontmatter (mode/model/temperature/permission/prompt/\u2026) is ALREADY valid v2 OpenCode behavior \u2014 its content stays unchanged. Only the file moves (section 5). The manifest conversion is governance-only.\n- The current layout: v1 projects keep agents in `.kortix/opencode/agents/`, skills in `.kortix/opencode/skills/`, the other OpenCode files in `.kortix/opencode/`, and memory in `.kortix/memory/`. A v1 manifest that sets `opencode.config_dir` uses that directory instead of `.kortix/opencode`.\n\n## 2. Bring the platform baseline up to date first\n\nA project still on a v1 manifest is usually also running stale platform skills. Refresh them BEFORE touching the manifest so the migration lands on a current baseline:\n\n1. `kortix marketplace updates` \u2014 a hash-diff report of every marketplace-tracked item (the kortix-managed skills like `kortix-system`/`kortix-memory`, plus any marketplace skills the user installed).\n2. If updates are listed, apply them: `kortix marketplace update --all`. **This commits directly to `main` through the platform's own hash-safe update path \u2014 it is intentionally NOT part of your change request.** It only rewrites files whose installed hash no longer matches the catalog, so untouched user files are never clobbered.\n3. Sync your session branch on top of the refreshed main: `git fetch origin && git rebase origin/main`.\n4. Items reported as `orphaned` (no longer in the catalog) are not updatable \u2014 leave them alone and mention them in the change request description.\n5. Marketplace-tracked items are the ONLY thing you refresh this way. Do NOT hand-update anything else \"to latest\": agents' `.md` files, the memory directory, `opencode.jsonc`, the continuation plugin, `tools/*.ts`, and custom skills are user- or platform-owned files with no update tracking \u2014 leave every one of them untouched. Section 5 moves some of them and rewrites their old paths; that is the only change they get.\n\n## 3. The authoritative schema is one command away\n\nWhenever you are unsure about a field name, an allowed value, or whether a key survived into v2, consult the canonical JSON Schema instead of guessing:\n\n- `kortix schema --version 2` \u2014 prints the exact v2 schema the validator and the CR-merge gate enforce. Works offline inside your sandbox. `kortix schema --version 1` prints the v1 shape you are migrating FROM.\n- The same documents are published at `https://kortix.com/schema/kortix.v2.schema.json` (and `kortix.v1.schema.json`, plus the combined `kortix.schema.json` that dispatches on `kortix_version`).\n\nThe schema, this prompt, and `kortix validate` all enforce the same rules \u2014 if they ever appear to disagree, trust `kortix validate`'s output and say so in the change request description.\n\n## 4. The v2 shape you're producing\n\nv2's `agents:` map is GOVERNANCE ONLY \u2014 connectors/secrets/skills/kortix_permissions/workspace/enabled. OpenCode behavior (mode/model/temperature/permission/the prompt itself) is NOT part of the manifest at all; it lives entirely in each agent's own `.md` frontmatter + body, exactly as it does today. After section 5 that file is `agents/<name>.md`, and the agent's `file:` key names it.\n\n```yaml\nkortix_version: 2\ndefault_agent: <name> # REQUIRED \u2014 must resolve to a declared, enabled agent below\n\nagents:\n <name>:\n file: agents/<name>.md # the agent's .md, repo-relative (section 5 moves it there)\n enabled: true # optional; false = can't start sessions (default true)\n connectors: all # connector slugs | \"all\" | \"none\"\n secrets: all # renamed from v1's \"env\" \u2014 names | \"all\" | \"none\"\n kortix_permissions: all # project.* permission names | \"all\" | \"none\"\n skills: all # names of skills/* this agent may invoke | \"all\" | \"none\"\n workspace:\n project_repository: false # optional \u2014 true (default) | false\n```\n\nThat's the WHOLE block. No `description`, no `model`, no `opencode:` sub-object, no `mode`/`temperature`/`permission`/`prompt` \u2014 every one of those is a hard schema error if authored here. They already live in the `.md` and are staying there.\n\nRules that the schema enforces (get these right or `kortix validate` fails):\n\n- `agents` is a MAP (`name: {...}`), not the v1 `[[agents]]` array of tables.\n- `default_agent` is required at the top level and must name a declared, enabled (`enabled` not `false`) agent.\n- Any behavioral field (`description`, `model`, `mode`, `temperature`, `top_p`, `steps`, `variant`, `color`, `hidden`, `permission`, `prompt`, or a nested `opencode:` block) authored on the manifest agent block is a hard error, pointing you at the agent's own `.md` frontmatter instead \u2014 because that's where it already lives, untouched.\n- `disable` is a hard error too \u2014 it's the manifest-governance `enabled` (inverted): write `enabled: false` instead. (This is unrelated to a NATIVE `disable` key that might already be hand-authored in an agent's own `.md` frontmatter \u2014 leave that alone; it's a different, runtime-level concept.)\n- `env` is a hard error in v2 \u2014 it is renamed `secrets`. **v2 defaults every omitted grant (`connectors`/`secrets`/`kortix_permissions`/`skills`) to `\"none\"` (deny-by-default), unlike v1 which defaulted an omitted grant to `\"all\"`.** To avoid silently narrowing an agent's access during migration, write the EXPLICIT value that reproduces today's behavior for every agent \u2014 if a v1 agent had no `[[agents]]` entry, or its `env`/`connectors`/`kortix_cli` (or `kortix_permissions`) were omitted or set to `all`, write `secrets: all`, `connectors: all`, `kortix_permissions: all` explicitly in its v2 block. Only narrow a grant if the v1 manifest already narrowed it (an explicit list, or `none`) \u2014 carry that exact list over. `skills` has no v1 equivalent; default new agents to `all` unless you have a specific reason to narrow.\n- `channels` is removed entirely in v2 \u2014 delete any `[[channels]]` block. Channel\u2194agent routing now lives in the dashboard (Customize \u2192 Channels), not in git. Do not try to replicate it in the manifest.\n- Every other top-level section (`project`, `env` for required/optional documentation vars \u2014 NOT the per-agent grant, `sandbox`, `triggers`, `connectors`, `apps`) keeps its v1 shape unchanged \u2014 translated to YAML, not restructured. `opencode.config_dir` is the one exception: section 5 sets it. If `triggers[].agent` names an agent, make sure that name still exists in the new `agents` map (rename references if you renamed an agent).\n- If an agent has no `.md` today (a bare `[[agents]]` entry with no matching OpenCode agent file), still declare it in `agents:` with its governance grants carried over \u2014 don't drop it. Omit its `file:` key. It has no behavior until someone adds `agents/<name>.md`.\n\n## 5. Move to the root layout\n\nv2 projects keep harness-neutral files at the repo root. Move this project in the same change request. Use `git mv`, so git records every move as a rename and keeps each file's history.\n\n| From | To |\n| --- | --- |\n| `.kortix/opencode/agents/<name>.md` | `agents/<name>.md` |\n| `.kortix/opencode/skills/<name>/` | `skills/<name>/` |\n| `.kortix/memory/` | `memory/` |\n| the rest of `.kortix/opencode/` (`opencode.jsonc`, `package.json`, `bun.lock`, `plugins/`, `tools/`, `commands/`) | `harnesses/opencode/` |\n\nIf the v1 manifest sets `opencode.config_dir`, that directory is the source instead of `.kortix/opencode`. Skip a row when its source does not exist.\n\n```\nmkdir -p harnesses\ngit mv .kortix/opencode/agents agents\ngit mv .kortix/opencode/skills skills\ngit mv .kortix/memory memory\ngit mv .kortix/opencode harnesses/opencode\n```\n\n`git mv <dir> <target>` moves `<dir>` INTO `<target>` when `<target>` already exists. Check each target with `ls` first. When it exists, move the entries one at a time (`git mv .kortix/opencode/skills/<name> skills/<name>`).\n\nThen:\n\n1. Set the OpenCode config dir in `kortix.yaml`: `opencode:` with `config_dir: harnesses/opencode`.\n2. Give every agent whose `.md` you moved a `file:` key: `file: agents/<name>.md`.\n3. Update `.gitignore`: rewrite each `.kortix/opencode/<path>` entry as `harnesses/opencode/<path>`, and each `.kortix/memory/<path>` entry as `memory/<path>`. Keep `.kortix/state/` \u2014 runtime state stays there.\n4. Find every remaining reference to an old path: `git grep -n -e '.kortix/opencode' -e '.kortix/memory'`. Replace each one with its new path. For example, `tools/memory.ts` hard-codes `.kortix/memory`, and agent prompts often name it. Change only the path. Leave `.kortix/state/` and `.kortix/link.json` references alone.\n\nEdge cases:\n\n- A root `agents/`, `memory/`, or `harnesses/` directory that already holds files unrelated to Kortix: do not merge into it. Skip this whole section, steps 1\u20134 included, keep the legacy layout (Kortix still reads it), and say why in the change request description.\n- A skill that already exists in `skills/<name>/` (the marketplace update in section 2 can install it there): keep the root copy and remove the legacy copy with `git rm -r`.\n\n## 6. Legacy keys v2 refuses \u2014 drop these while you convert\n\nv1 tolerates several retired keys with a deprecation warning; v2 makes every one of them a hard error. Remove them as part of the conversion and note each removal in the change request description:\n\n- **Retired `kortix_permissions` actions** \u2014 `project.session.exec`, `project.gateway.routing.edit`, `project.schedule.read`, `project.schedule.write`, `project.webhook.read`, `project.webhook.write`, `channel.read`, `channel.connect`, `channel.send`, `channel.disconnect`. These were removed from the enforcement catalog and have been no-ops for a while \u2014 granting or omitting them never had any effect, so deleting them from a grant list changes nothing. Do NOT substitute a broader grant (e.g. `all`) to \"cover\" a deleted action.\n- **`credential = \"per_user\"` on a `[[connectors]]` entry** \u2014 the per-user credential mode was removed; every connector is `\"shared\"` now. Delete the `credential` key (or write `shared` explicitly if the entry already spelled it out).\n- **`agent_scope` on a `[[connectors]]` entry** \u2014 retired; the runtime no longer reads it. Per-agent connector access is expressed from the OTHER side now: each agent's `connectors:` grant in the `agents:` map. If a v1 connector had `agent_scope = [\"a\", \"b\"]`, make sure agents outside that list don't get that connector slug in their `connectors` grant (use an explicit slug list instead of `all` for the agents that should keep access), then delete the key.\n- **Legacy singular `[sandbox]` image keys** (`image`, `dockerfile`, `cpu`, `memory`, `disk`, \u2026) \u2014 already an error in v1's validator; if `kortix validate` flags them, move the image definition under `[[sandbox.templates]]` \u2192 `sandbox.templates:` with a named slug.\n\n## 7. Worked example\n\nA representative v1 `kortix.toml`:\n\n```toml\nkortix_version = 1\n\n[project]\nname = \"acme-ops\"\n\n[env]\nrequired = [\"DATABASE_URL\"]\n\n[[agents]]\nname = \"dev\"\nconnectors = [\"github\", \"linear\"]\nenv = \"all\"\nkortix_cli = [\"project.file.read\", \"project.file.write\", \"project.session.exec\"]\n\n[[agents]]\nname = \"support\"\n# no grants declared \u2014 v1 treats omitted grants as \"all\"\n\n[[channels]]\ntype = \"slack\"\nagent = \"support\"\n\n[[triggers]]\nslug = \"weekly-summary\"\ntype = \"cron\"\ncron = \"0 9 * * 1\"\nagent = \"dev\"\nprompt = \"Post the weekly summary.\"\n\n[[connectors]]\nslug = \"github\"\nprovider = \"github\"\ncredential = \"per_user\"\nagent_scope = [\"dev\"]\n```\n\nbecomes this v2 `kortix.yaml`:\n\n```yaml\nkortix_version: 2\ndefault_agent: dev\n\nproject:\n name: acme-ops\n\nenv:\n required:\n - DATABASE_URL\n\nopencode:\n config_dir: harnesses/opencode\n\nagents:\n dev:\n file: agents/dev.md\n connectors:\n - github\n - linear\n secrets: all # v1 \"env = all\", renamed\n kortix_permissions: # renamed from v1's kortix_cli; project.session.exec dropped \u2014 retired no-op action\n - project.file.read\n - project.file.write\n skills: all\n support:\n file: agents/support.md\n # v1 had no grants (implicit all) \u2014 but the github connector was\n # agent_scoped to dev only, so \"all\" would WIDEN support's access.\n # An explicit list preserves today's effective behavior instead.\n connectors: none\n secrets: all\n kortix_permissions: all\n skills: all\n\ntriggers:\n - slug: weekly-summary\n type: cron\n cron: \"0 9 * * 1\"\n agent: dev\n prompt: Post the weekly summary.\n\nconnectors:\n - slug: github\n provider: github\n # credential/agent_scope removed \u2014 connectors are shared; per-agent\n # access now lives in the agents map above.\n```\n\nNote what happened: the `[[channels]]` block is gone (dashboard-owned now), each agent names its moved `.md` with `file:`, `opencode.config_dir` points at `harnesses/opencode`, `env` became `secrets`, the retired CLI action and connector keys were dropped, every omitted-in-v1 grant was written out explicitly, and the old `agent_scope` was honored by adjusting the AGENTS' `connectors` grants rather than copied over. Your project will differ \u2014 apply the rules, not this output verbatim.\n\n## 8. Leave every agent's `.md` alone\n\nBeyond the move and the path updates in section 5, do not edit or reformat any agent's `.md` as part of this migration. Its frontmatter (mode/model/permission/temperature/\u2026) and body (the system prompt) are ALREADY the agent's v2 behavior \u2014 nothing else about them needs to change. This is what keeps the manifest conversion governance-only and comparatively small: you're translating one array-of-tables into one map of governance grants, full stop.\n\n## 9. Write the file, remove the old one\n\n- Write the fully assembled manifest to `kortix.yaml` at the repo root (same directory as the old `kortix.toml`).\n- If the project has more than ~10 triggers, or long multi-line prompts, keep `kortix.yaml` readable: add `imports: [triggers/]` to the root and write the triggers into YAML files under `triggers/` (one file per trigger or per group; each file is a `triggers:` list). Every other key stays in `kortix.yaml`. Slugs must stay unique across all files.\n- Carry over meaningful TOML comments as YAML comments next to the same keys \u2014 hand-written context in a manifest is documentation someone chose to leave; don't strip it.\n- Delete the old `kortix.toml` in the same commit \u2014 don't leave both files (the platform always prefers `kortix.yaml` when both exist, but a stale v1 file next to it is confusing for the next person who edits by hand).\n- You do not need to touch any project setting outside git \u2014 the platform resolves `kortix.yaml` automatically once it exists, regardless of the configured manifest filename.\n\n## 10. Validate before you're done\n\nRun `kortix validate` (it auto-detects `kortix.yaml`). Fix every error it reports \u2014 do not open the change request with a manifest that fails validation. Warnings are fine to leave if they're informational, but read them. If an error surprises you, cross-check the field against `kortix schema --version 2`.\n\n## 11. Land it as a change request \u2014 never merge\n\nFirst, re-sync: `git fetch origin` and check whether `origin/main` advanced while you worked (`git log HEAD..origin/main --oneline`) \u2014 on active projects it will (connectors added from the dashboard, other sessions merging). If it moved, rebase; if the rebase conflicts on `kortix.toml` (main changed the manifest you deleted), don't fight it \u2014 `git rebase --abort`, `git reset --hard origin/main`, and redo the conversion against the CURRENT manifest, then continue. Never revert main's changes to win a conflict.\n\nThen commit, **push the branch**, and open the change request. A commit that is never pushed leaves the CR empty (\"No changes detected\") and un-appliable \u2014 the platform refuses such a CR outright (`422 CR_HEAD_NOT_AHEAD`):\n\n```\ngit add -A && git commit -m \"Migrate to kortix_version 2 (kortix.yaml) and the root layout\"\ngit push origin HEAD\nkortix cr open --head <your-branch> --title \"Migrate to kortix_version 2 (kortix.yaml) and the root layout\" --description \"<what you converted, which files you moved, which agent you picked as default_agent and why, every legacy key you removed, and any grant you had to leave narrowed>\"\n```\n\nIf the push is rejected because the remote session branch moved, `git fetch origin` then `git push --force-with-lease origin HEAD` \u2014 your own session branch only, never any other branch.\n\nThen verify the CR actually carries your diff: run `kortix cr diff <number>` \u2014 if it reports no changes, your push didn't land; push again and re-check (the CR updates automatically, do not open a second one).\n\nDo **not** run `kortix cr merge`. This is a human-reviewed change like any other \u2014 stop once the CR is open and verified non-empty, and tell the user its number so they can review the diff and merge it themselves.";
@@ -24,6 +24,7 @@
24
24
  * Kept as a plain exported constant (not inlined at the call site) so it's
25
25
  * independently testable and diffable — this is the load-bearing artifact of
26
26
  * the feature, not the wiring around it.
27
+ * @deprecated Moves a v1 project to the OpenCode-era v2 layout (harnesses/opencode). OpenCode is sunset (2026-10-23); migrate with `kortix migrate` to kortix_version 3.
27
28
  */
28
29
  export const MIGRATE_TO_V2_PROMPT = `Migrate this project's manifest from kortix_version 1 (kortix.toml) to kortix_version 2 (kortix.yaml), and move its files to the root layout. Read everything first, then make the change, then land it as a change request — do not merge it yourself.
29
30
 
@@ -280,7 +280,6 @@ function BindingOverrides({ binding, policies, }) {
280
280
  return (_jsxs("span", { title: describeEffectiveModel(binding), className: cn('inline-flex items-center gap-1', overrides && 'text-foreground'), children: [modelUnavailable ? (_jsx(WarningCircleIcon, { weight: "fill", className: "text-kortix-orange size-3.5 shrink-0" })) : null, overrides || tI18nComplete.raw('texte8cb80e5c5cb')] }));
281
281
  }
282
282
  /** The schedule modal's field frame, around each picker in the settings dialog. */
283
- const FIELD_FRAME = 'bg-popover flex w-full items-center rounded-md border px-2 py-1.5';
284
283
  /** Rows before the list grows a search field — the `AgentSelector` rule (7). */
285
284
  const BINDING_SEARCH_MIN = 7;
286
285
  const CONVERSATION_POLICIES = [
@@ -377,9 +376,6 @@ function ChannelSettingsModalContent({ projectId, binding, name, scope, projectD
377
376
  const policy = policyDraft ?? binding.conversationPolicy;
378
377
  const patch = channelSettingsPatch({ agentName: binding.agentName, model: boundModel, conversationPolicy: binding.conversationPolicy }, { agentName, model, conversationPolicy: policy });
379
378
  const dirty = Object.keys(patch).length > 0;
380
- // The pickers refuse to open when disabled but do not dim; dim them like the
381
- // disabled Select, which dims itself.
382
- const pickerFrame = cn(FIELD_FRAME, !canWrite && 'opacity-50');
383
379
  const update = useUpdateChannelBinding();
384
380
  const save = (e) => {
385
381
  e.preventDefault();
@@ -393,11 +389,11 @@ function ChannelSettingsModalContent({ projectId, binding, name, scope, projectD
393
389
  onError: (error) => errorToastFallback(error, tI18nComplete),
394
390
  });
395
391
  };
396
- return (_jsxs(ModalContent, { className: "lg:max-w-lg", children: [_jsxs(ModalHeader, { children: [_jsx(ModalTitle, { children: tI18nComplete.raw('textafc219f2a1e7') }), _jsx(ModalDescription, { children: tI18nComplete.raw('text95112f275fba') })] }), _jsxs("form", { onSubmit: save, children: [_jsxs(ModalBody, { className: "max-h-[calc(100dvh-15rem)] space-y-5 overflow-y-auto pt-4", children: [_jsxs("div", { className: "bg-popover flex min-w-0 items-center gap-3 rounded-md border px-3 py-2.5", children: [_jsx(BindingBrandMark, { platform: binding.platform }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-sm font-medium", title: binding.channelId, children: name }), _jsx("p", { className: "text-muted-foreground truncate text-xs", children: scope })] })] }), _jsxs(FieldGroup, { className: "gap-5", children: [_jsxs(Field, { "data-disabled": !canWrite, children: [_jsx(FieldTitle, { children: tI18nComplete.raw('text11b39c93777e') }), _jsx("div", { className: pickerFrame, children: _jsx(AgentSelector, { agents: agents, selectedAgent: agentName ?? defaultAgentLabel, onSelect: (v) => setAgentDraft(!v || v === defaultAgentLabel ? null : v), disabled: !canWrite }) }), _jsx(FieldDescription, { className: "text-xs", children: tI18nComplete.raw('text12dc6dbd8fc8') })] }), _jsxs(Field, { "data-disabled": !canWrite, children: [_jsx(FieldTitle, { children: tI18nComplete.raw('text5e2c614c23f0') }), _jsx("div", { className: pickerFrame, children: _jsx(ModelSelector, { models: models, providers: providers, selectedModel: model, unsetLabel: tI18nComplete.raw('texte8cb80e5c5cb'), onSelect: setModelDraft, disabled: !canWrite }) }), _jsxs(FieldDescription, { className: "text-xs", children: [modelDraft === undefined &&
392
+ return (_jsxs(ModalContent, { className: "lg:max-w-lg", children: [_jsxs(ModalHeader, { children: [_jsx(ModalTitle, { children: tI18nComplete.raw('textafc219f2a1e7') }), _jsx(ModalDescription, { children: tI18nComplete.raw('text95112f275fba') })] }), _jsxs("form", { onSubmit: save, children: [_jsxs(ModalBody, { className: "max-h-[calc(100dvh-15rem)] space-y-5 overflow-y-auto pt-4", children: [_jsxs("div", { className: "bg-popover flex min-w-0 items-center gap-3 rounded-md border px-3 py-2.5", children: [_jsx(BindingBrandMark, { platform: binding.platform }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-sm font-medium", title: binding.channelId, children: name }), _jsx("p", { className: "text-muted-foreground truncate text-xs", children: scope })] })] }), _jsxs(FieldGroup, { className: "gap-5", children: [_jsxs(Field, { "data-disabled": !canWrite, children: [_jsx(FieldTitle, { children: tI18nComplete.raw('text11b39c93777e') }), _jsx(AgentSelector, { agents: agents, selectedAgent: agentName ?? defaultAgentLabel, onSelect: (v) => setAgentDraft(!v || v === defaultAgentLabel ? null : v), disabled: !canWrite, appearance: "field" }), _jsx(FieldDescription, { className: "text-xs", children: tI18nComplete.raw('text12dc6dbd8fc8') })] }), _jsxs(Field, { "data-disabled": !canWrite, children: [_jsx(FieldTitle, { children: tI18nComplete.raw('text5e2c614c23f0') }), _jsx(ModelSelector, { models: models, providers: providers, selectedModel: model, unsetLabel: tI18nComplete.raw('texte8cb80e5c5cb'), onSelect: setModelDraft, disabled: !canWrite, appearance: "field" }), _jsxs(FieldDescription, { className: "text-xs", children: [modelDraft === undefined &&
397
393
  binding.effectiveModel.source !== 'explicit' &&
398
394
  (binding.opencodeModel || binding.effectiveModel.model)
399
395
  ? `${describeEffectiveModel(binding)}. `
400
- : null, tI18nComplete.raw('text9116af1ce384')] })] }), _jsxs(Field, { "data-disabled": !canWrite, children: [_jsx(FieldTitle, { children: tI18nComplete.raw('textb1ca871c6696') }), _jsx("div", { className: FIELD_FRAME, children: _jsxs(Select, { value: policy, onValueChange: (v) => setPolicyDraft(v), disabled: !canWrite, children: [_jsx(SelectTrigger, { variant: "transparent", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: conversationPolicies.map((p) => (_jsx(SelectItem, { value: p.value, children: p.label }, p.value))) })] }) }), _jsx(FieldDescription, { className: "text-xs", children: policyDescription(policy, tI18nComplete) })] })] })] }), _jsxs(ModalFooter, { className: "pt-2 pb-5 sm:justify-between", children: [_jsxs(Button, { type: "button", variant: "outline-ghost", className: "gap-1", onClick: onBack, children: [_jsx(CaretLeftIcon, { className: "size-3.5 shrink-0" }), tI18nComplete.raw('text76900f1bfd16')] }), canWrite ? (_jsxs(Button, { type: "submit", disabled: !dirty || update.isPending, children: [update.isPending ? _jsx(Loading, { className: "size-4 shrink-0" }) : null, tI18nComplete.raw('text1509f561f241')] })) : null] })] })] }));
396
+ : null, tI18nComplete.raw('text9116af1ce384')] })] }), _jsxs(Field, { "data-disabled": !canWrite, children: [_jsx(FieldTitle, { children: tI18nComplete.raw('textb1ca871c6696') }), _jsxs(Select, { value: policy, onValueChange: (v) => setPolicyDraft(v), disabled: !canWrite, children: [_jsx(SelectTrigger, { className: "h-9 w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: conversationPolicies.map((p) => (_jsx(SelectItem, { value: p.value, children: p.label }, p.value))) })] }), _jsx(FieldDescription, { className: "text-xs", children: policyDescription(policy, tI18nComplete) })] })] })] }), _jsxs(ModalFooter, { className: "pt-2 pb-5 sm:justify-between", children: [_jsxs(Button, { type: "button", variant: "outline-ghost", className: "gap-1", onClick: onBack, children: [_jsx(CaretLeftIcon, { className: "size-3.5 shrink-0" }), tI18nComplete.raw('text76900f1bfd16')] }), canWrite ? (_jsxs(Button, { type: "submit", disabled: !dirty || update.isPending, children: [update.isPending ? _jsx(Loading, { className: "size-4 shrink-0" }) : null, tI18nComplete.raw('text1509f561f241')] })) : null] })] })] }));
401
397
  }
402
398
  /** A conversation's display name: the Slack or Teams name, else its kind. */
403
399
  function bindingName(binding, tI18nComplete) {