@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
@@ -0,0 +1,115 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button } from '../../primitives/button.js';
4
+ import { Field, FieldGroup, FieldLabel } from '../../primitives/field.js';
5
+ import { Input } from '../../primitives/input.js';
6
+ import { InputGroup, InputGroupAddon, InputGroupInput } from '../../primitives/input-group.js';
7
+ import Loading from '../../primitives/loading.js';
8
+ import { useTranslations } from '../../i18n/index.js';
9
+ import { getConnectorAppForm, HostBoundaryError, submitConnectorAppForm, } from '@kortix/sdk';
10
+ import { ArrowUpRightIcon, CheckIcon, ClockCountdownIcon, LinkBreakIcon, WarningCircleIcon, } from '@phosphor-icons/react';
11
+ import { useEffect, useState } from 'react';
12
+ import { setupLinkApiBase } from './api-base.js';
13
+ import { StatusNotice } from './secret-intake-form.js';
14
+ // Machine values, not prose: Shopify serves every store at <handle>.myshopify.com.
15
+ const SHOPIFY_HOST_SUFFIX = '.myshopify.com';
16
+ const EXAMPLE_STORE_HANDLE = 'acme';
17
+ const DEV_DASHBOARD_URL = 'https://dev.shopify.com/dashboard';
18
+ /** Long enough to read the confirmation before the window closes itself. */
19
+ const CLOSE_AFTER_MS = 1_500;
20
+ function statusOf(cause) {
21
+ return cause instanceof HostBoundaryError ? cause.status : null;
22
+ }
23
+ function DashboardLink({ children }) {
24
+ return (_jsxs("a", { href: DEV_DASHBOARD_URL, target: "_blank", rel: "noopener noreferrer", className: "group text-kortix-base focus-visible:ring-ring inline-flex items-baseline gap-0.5 rounded-sm font-medium outline-none focus-visible:ring-2", children: [_jsx("span", { className: "decoration-kortix-base/40 group-hover:decoration-kortix-base underline underline-offset-2", children: children }), _jsx(ArrowUpRightIcon, { className: "size-3 shrink-0 self-center" })] }));
25
+ }
26
+ /**
27
+ * The form behind an own-app connect link (`/connect/app/[token]`): the app's
28
+ * client credentials and the store, for a Composio toolkit that runs on the
29
+ * customer's own app. Shopify is the one such app today.
30
+ *
31
+ * The page opens in the connect popup. The window that opened it polls the
32
+ * connection, so after a success this window only confirms and closes itself
33
+ * (or follows the native app's return link).
34
+ */
35
+ export function AppCredentialsForm({ token }) {
36
+ const t = useTranslations('connectAppForm');
37
+ const base = setupLinkApiBase();
38
+ const [phase, setPhase] = useState('loading');
39
+ const [info, setInfo] = useState(null);
40
+ const [store, setStore] = useState('');
41
+ const [clientId, setClientId] = useState('');
42
+ const [clientSecret, setClientSecret] = useState('');
43
+ const [connectedAs, setConnectedAs] = useState(null);
44
+ const [message, setMessage] = useState(null);
45
+ useEffect(() => {
46
+ let cancelled = false;
47
+ getConnectorAppForm(token, { backendUrl: base }).then((body) => {
48
+ if (cancelled)
49
+ return;
50
+ setInfo(body);
51
+ setConnectedAs(body.connected_as);
52
+ setPhase(body.connected ? 'done' : 'ready');
53
+ }, (cause) => {
54
+ if (cancelled)
55
+ return;
56
+ const status = statusOf(cause);
57
+ setMessage(cause instanceof Error ? cause.message : null);
58
+ setPhase(status === 410 ? 'expired' : status === 404 ? 'invalid' : status === 409 ? 'stale' : 'error');
59
+ });
60
+ return () => {
61
+ cancelled = true;
62
+ };
63
+ }, [base, token]);
64
+ async function submit() {
65
+ if (!store.trim() || !clientId.trim() || !clientSecret.trim()) {
66
+ setMessage(t.raw('missingFields'));
67
+ return;
68
+ }
69
+ setPhase('submitting');
70
+ setMessage(null);
71
+ try {
72
+ const result = await submitConnectorAppForm(token, { store: store.trim(), clientId: clientId.trim(), clientSecret: clientSecret.trim() }, { backendUrl: base });
73
+ setConnectedAs(result.connected_as);
74
+ setPhase('done');
75
+ window.setTimeout(() => {
76
+ if (result.success_redirect_uri)
77
+ window.location.assign(result.success_redirect_uri);
78
+ else
79
+ window.close();
80
+ }, CLOSE_AFTER_MS);
81
+ }
82
+ catch (cause) {
83
+ const status = statusOf(cause);
84
+ setMessage(cause instanceof Error ? cause.message : t.raw('failed'));
85
+ if (status === 410)
86
+ setPhase('expired');
87
+ else if (status === 409)
88
+ setPhase('stale');
89
+ else
90
+ setPhase('ready');
91
+ }
92
+ }
93
+ const app = info?.name ?? 'Shopify';
94
+ switch (phase) {
95
+ case 'loading':
96
+ return (_jsxs("div", { role: "status", className: "text-muted-foreground flex items-center justify-center gap-2 py-8 text-sm", children: [_jsx(Loading, { className: "size-4 shrink-0" }), t.raw('loading')] }));
97
+ case 'expired':
98
+ return (_jsx(StatusNotice, { icon: ClockCountdownIcon, tone: "warning", title: t.raw('expiredTitle'), description: t.raw('startAgain') }));
99
+ case 'invalid':
100
+ return (_jsx(StatusNotice, { icon: LinkBreakIcon, tone: "destructive", title: t.raw('invalidTitle'), description: t.raw('startAgain') }));
101
+ case 'stale':
102
+ return (_jsx(StatusNotice, { icon: LinkBreakIcon, tone: "warning", title: t.raw('staleTitle'), description: message ?? t.raw('startAgain') }));
103
+ case 'error':
104
+ return _jsx(StatusNotice, { icon: WarningCircleIcon, tone: "destructive", title: message ?? t.raw('failed') });
105
+ case 'done':
106
+ return (_jsx(StatusNotice, { icon: CheckIcon, tone: "success", title: t('connectedTitle', { app }), description: connectedAs ? t('connectedDescription', { store: connectedAs }) : t.raw('closeWindow') }));
107
+ }
108
+ const submitting = phase === 'submitting';
109
+ return (_jsxs("div", { className: "space-y-5", children: [_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-foreground text-sm font-medium", children: t.raw('stepsTitle') }), _jsxs("ol", { className: "text-muted-foreground list-decimal space-y-1 pl-4 text-xs text-pretty", children: [_jsx("li", { children: t.rich('step1', { link: (chunks) => _jsx(DashboardLink, { children: chunks }) }) }), _jsx("li", { children: t.raw('step2') }), _jsx("li", { children: t.raw('step3') }), _jsx("li", { children: t.raw('step4') })] }), _jsx("p", { className: "text-muted-foreground text-xs text-pretty", children: t.raw('sameOrganization') })] }), _jsxs(FieldGroup, { className: "gap-5", children: [_jsxs(Field, { className: "space-y-2", children: [_jsx(FieldLabel, { htmlFor: "app-form-store", className: "text-foreground", children: t.raw('storeLabel') }), _jsxs(InputGroup, { children: [_jsx(InputGroupInput, { id: "app-form-store", name: "store", autoComplete: "off", spellCheck: false,
110
+ // 16px below `sm` so iOS Safari does not zoom the page on focus.
111
+ className: "font-mono max-sm:text-base", placeholder: EXAMPLE_STORE_HANDLE, value: store, disabled: submitting, onChange: (e) => setStore(e.target.value) }), _jsx(InputGroupAddon, { align: "inline-end", children: SHOPIFY_HOST_SUFFIX })] })] }), _jsxs(Field, { className: "space-y-2", children: [_jsx(FieldLabel, { htmlFor: "app-form-client-id", className: "text-foreground", children: t.raw('clientIdLabel') }), _jsx(Input, { id: "app-form-client-id", name: "client_id", autoComplete: "off", spellCheck: false, className: "font-mono max-sm:text-base", value: clientId, disabled: submitting, onChange: (e) => setClientId(e.target.value) })] }), _jsxs(Field, { className: "space-y-2", children: [_jsx(FieldLabel, { htmlFor: "app-form-client-secret", className: "text-foreground", children: t.raw('clientSecretLabel') }), _jsx(Input, { id: "app-form-client-secret", name: "client_secret", type: "password", autoComplete: "off", spellCheck: false, className: "font-mono max-sm:text-base", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", value: clientSecret, disabled: submitting, onChange: (e) => setClientSecret(e.target.value), onKeyDown: (e) => {
112
+ if (e.key === 'Enter')
113
+ void submit();
114
+ } })] })] }), _jsxs("div", { className: "space-y-3", children: [message ? (_jsx("p", { role: "alert", className: "text-destructive text-xs text-pretty", children: message })) : null, _jsxs(Button, { type: "button", className: "w-full", onClick: submit, disabled: submitting, children: [submitting ? _jsx(Loading, { className: "size-4 shrink-0" }) : null, submitting ? t.raw('connecting') : t('submit', { app })] }), _jsx("p", { className: "text-muted-foreground text-center text-xs text-pretty", children: t.raw('secretNote') })] })] }));
115
+ }
@@ -1,3 +1,13 @@
1
+ import type { OutcomeTone } from '../../internal/outcomes/outcome-types.js';
2
+ import { type Icon } from '@phosphor-icons/react';
3
+ import { type ReactElement } from 'react';
4
+ /** Every phase that is not the form: tinted tile, title, one muted line. */
5
+ export declare function StatusNotice({ icon: Glyph, tone, title, description, }: {
6
+ icon: Icon;
7
+ tone: OutcomeTone;
8
+ title: string;
9
+ description?: string;
10
+ }): ReactElement;
1
11
  /**
2
12
  * Renders the fields an agent-minted secret link asks for, and submits the
3
13
  * values the human types. Shared by the public /secret-intake/[token] page and
@@ -14,7 +14,7 @@ import { Fragment, useEffect, useState } from 'react';
14
14
  import { classifySetupLinkError, describeLinkExpiry, describeWithheldSecrets, splitTextLinks, } from './util.js';
15
15
  import { setupLinkApiBase } from './api-base.js';
16
16
  /** Every phase that is not the form: tinted tile, title, one muted line. */
17
- function StatusNotice({ icon: Glyph, tone, title, description, }) {
17
+ export function StatusNotice({ icon: Glyph, tone, title, description, }) {
18
18
  const tint = outcomeTint(tone);
19
19
  return (_jsxs("div", { role: "status", className: "flex flex-col items-center gap-3 py-8 text-center", children: [_jsx("span", { className: cn('flex size-9 items-center justify-center rounded-sm ring-1', tint.ring, tint.bg), children: _jsx(Glyph, { weight: "fill", className: cn('size-5', tint.fg) }) }), _jsxs("div", { className: "max-w-xs space-y-1", children: [_jsx("p", { className: "text-foreground text-sm font-medium", children: title }), description ? (_jsx("p", { className: "text-muted-foreground text-xs text-pretty", children: description })) : null] })] }));
20
20
  }
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from 'react';
1
2
  import type { Icon } from './kortix-icons.js';
2
3
  import { type Icon as IconType } from '@phosphor-icons/react';
3
4
  /**
@@ -79,6 +80,8 @@ export interface EntityAvatarProps {
79
80
  icon?: Icon | IconType;
80
81
  size?: EntityAvatarSize;
81
82
  className?: string;
83
+ /** Shows on hover, through `Hint`: who or what this avatar stands for. */
84
+ hint?: ReactNode;
82
85
  }
83
- export declare function EntityAvatar({ label, glyph, emoji, icon: IconComponent, size, className, }: EntityAvatarProps): import("react").JSX.Element;
86
+ export declare function EntityAvatar({ hint, label, glyph, emoji, icon: IconComponent, size, className, }: EntityAvatarProps): import("react").JSX.Element;
84
87
  export {};
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Hint } from './hint.js';
3
4
  import { emojiTint } from './emoji-tint.js';
4
5
  import { glyphFace } from './glyph-face.js';
5
6
  import { glyphForeground, glyphTint } from './glyph-tint.js';
@@ -48,12 +49,12 @@ function resolveGlyph(glyph) {
48
49
  const GlyphComponent = glyphFace(glyph.name);
49
50
  return GlyphComponent ? { GlyphComponent, color: glyph.color } : null;
50
51
  }
51
- export function EntityAvatar({ label, glyph, emoji, icon: IconComponent, size = 'md', className, }) {
52
+ export function EntityAvatar({ hint, label, glyph, emoji, icon: IconComponent, size = 'md', className, }) {
52
53
  const sizes = SIZE_MAP[size];
53
54
  const initial = (label?.trim()?.charAt(0) || '?').toUpperCase();
54
55
  const chalk = chalkColors(`${label?.trim()}` || initial);
55
56
  const resolvedGlyph = resolveGlyph(glyph);
56
- return (_jsx("span", { "data-slot": "entity-avatar",
57
+ const tile = (_jsx("span", { "data-slot": "entity-avatar",
57
58
  // chalkColors() is an inline style, so it beats any class a caller
58
59
  // passes. A glyph or an emoji is already the colour — sitting either on
59
60
  // a saturated hash-derived pastel reads as noise, and in dark mode that
@@ -103,4 +104,5 @@ export function EntityAvatar({ label, glyph, emoji, icon: IconComponent, size =
103
104
  // before the label that says the same thing. Same treatment as the
104
105
  // picker trigger in packages/ui/src/workspace/projects/modal/project-icon-field.tsx.
105
106
  _jsx("span", { "aria-hidden": true, className: "leading-none", children: emoji })) : IconComponent ? (_jsx(IconComponent, { className: sizes.icon })) : (initial) }));
107
+ return hint ? (_jsx(Hint, { side: "top", sideOffset: 6, alignOffset: 0, label: hint, children: tile })) : (tile);
106
108
  }
@@ -42,6 +42,8 @@ export interface UserAvatarProps {
42
42
  className?: string;
43
43
  ring?: boolean;
44
44
  variant?: 'default' | 'primary';
45
+ /** Shows on hover, through `Hint`: usually the person's name or email. */
46
+ hint?: React.ReactNode;
45
47
  }
46
- export declare function UserAvatar({ email, name, avatarUrl, size, variant, className, ring, }: UserAvatarProps): React.JSX.Element;
48
+ export declare function UserAvatar({ hint, email, name, avatarUrl, size, variant, className, ring, }: UserAvatarProps): React.JSX.Element;
47
49
  export {};
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Hint } from './hint.js';
3
4
  import { Avatar, AvatarFallback, AvatarImage } from './avatar.js';
4
5
  import { cn } from '../lib/utils.js';
5
6
  import { AVATAR_INK, avatarFill } from '../_vendor/kortix-shared.js';
@@ -34,13 +35,14 @@ const SIZE_MAP = {
34
35
  lg: { box: 'size-10', text: 'text-sm', initials: 2 },
35
36
  xl: { box: 'size-14', text: 'text-base', initials: 2 },
36
37
  };
37
- export function UserAvatar({ email, name, avatarUrl, size = 'md', variant = 'default', className, ring = false, }) {
38
+ export function UserAvatar({ hint, email, name, avatarUrl, size = 'md', variant = 'default', className, ring = false, }) {
38
39
  const sizes = SIZE_MAP[size] ?? SIZE_MAP.md;
39
40
  const initials = React.useMemo(() => initialsFromIdentity(name ?? undefined, email || '').slice(0, sizes.initials), [name, email, sizes.initials]);
40
41
  // Avatar v2: a solid fill keyed on the email, so a rename keeps the colour.
41
42
  const fill = avatarFill(email || name || '');
42
- return (_jsxs(Avatar, { className: cn(sizes.box, 'shrink-0 overflow-hidden rounded-sm p-0 font-medium', ring && 'ring-background ring-2',
43
+ const avatar = (_jsxs(Avatar, { className: cn(sizes.box, 'shrink-0 overflow-hidden rounded-sm p-0 font-medium', ring && 'ring-background ring-2',
43
44
  // A photo keeps an edge on a light page: a 1px inset hairline over it.
44
45
  avatarUrl &&
45
46
  'after:ring-border after:pointer-events-none after:absolute after:inset-0 after:rounded-sm after:ring-1 after:ring-inset', variant === 'primary' && 'bg-primary text-primary-foreground', className), children: [avatarUrl ? _jsx(AvatarImage, { src: avatarUrl, alt: name || email }) : null, _jsx(AvatarFallback, { className: cn('rounded-sm font-medium', sizes.text), style: { backgroundColor: fill, color: AVATAR_INK }, children: initials || '?' })] }));
47
+ return hint ? (_jsx(Hint, { side: "top", sideOffset: 6, alignOffset: 0, label: hint, children: avatar })) : (avatar);
46
48
  }
@@ -145,7 +145,7 @@ export interface UiHostConfig {
145
145
  terminalEnabled: boolean;
146
146
  /**
147
147
  * The Terminal panel offers "Connect from your machine": the deployment's
148
- * CLI installer (`<origin>/install`) and `kortix sessions connect`. A host
148
+ * CLI installer (`<origin>/install`) and `kortix sessions shell`. A host
149
149
  * that serves no installer and ships no Kortix CLI sets false, and the bar
150
150
  * is hidden. Default: true.
151
151
  */
@@ -1,7 +1,7 @@
1
1
  import type { Agent } from '@kortix/sdk/react';
2
2
  /** The label `AgentSelector` shows for a selection (null = nothing picked). Other surfaces that name the agent reuse it. */
3
3
  export declare function agentDisplayLabel(agents: Agent[], selectedAgent: string | null): string;
4
- export declare function AgentSelector({ agents, selectedAgent, onSelect, disabled, triggerLabelClassName, unavailableHint, open: controlledOpen, onOpenChange, }: {
4
+ export declare function AgentSelector({ agents, selectedAgent, onSelect, disabled, triggerLabelClassName, unavailableHint, open: controlledOpen, onOpenChange, appearance, }: {
5
5
  agents: Agent[];
6
6
  selectedAgent: string | null;
7
7
  onSelect: (agentName: string | null) => void;
@@ -17,4 +17,10 @@ export declare function AgentSelector({ agents, selectedAgent, onSelect, disable
17
17
  unavailableHint?: string | null;
18
18
  open?: boolean;
19
19
  onOpenChange?: (open: boolean) => void;
20
+ /**
21
+ * `chip` is the composer's borderless toolbar chip. `field` is a form field:
22
+ * the full-width `h-9` box every `SelectTrigger` in a form draws, so the
23
+ * picker reads as one more field instead of a chip inside a frame.
24
+ */
25
+ appearance?: 'chip' | 'field';
20
26
  }): import("react").JSX.Element;
@@ -7,8 +7,10 @@ import Hint from '../../primitives/hint.js';
7
7
  import { useTranslations } from '../../i18n/index.js';
8
8
  import { useUiHostConfig } from '../../providers/ui-provider.js';
9
9
  import { cn } from '../../lib/utils.js';
10
+ import { TRIGGER_CARET_CLASS, triggerVariants } from '../../primitives/trigger-variants.js';
10
11
  import { capitalizeWords, isMetaAgentName, META_AGENT_DISPLAY_NAME } from '../../_vendor/kortix-shared.js';
11
- import { CaretDownIcon, CheckIcon, SparkleIcon } from '@phosphor-icons/react';
12
+ import { CaretDownIcon, CheckIcon } from '@phosphor-icons/react';
13
+ import { AgentMark } from '../../internal/components/agent-mark.js';
12
14
  import { useEffect, useMemo, useState } from 'react';
13
15
  import { composerSelectableAgents } from './composer-agent-access.js';
14
16
  /**
@@ -36,7 +38,7 @@ export function agentDisplayLabel(agents, selectedAgent) {
36
38
  return 'Agent';
37
39
  return isMetaAgentName(name) ? META_AGENT_DISPLAY_NAME : capitalizeWords(name);
38
40
  }
39
- export function AgentSelector({ agents, selectedAgent, onSelect, disabled = false, triggerLabelClassName, unavailableHint, open: controlledOpen, onOpenChange, }) {
41
+ export function AgentSelector({ agents, selectedAgent, onSelect, disabled = false, triggerLabelClassName, unavailableHint, open: controlledOpen, onOpenChange, appearance = 'chip', }) {
40
42
  const tHardcodedUi = useTranslations('hardcodedUi');
41
43
  const t = useTranslations('threads');
42
44
  const [internalOpen, setInternalOpen] = useState(false);
@@ -84,7 +86,13 @@ export function AgentSelector({ agents, selectedAgent, onSelect, disabled = fals
84
86
  // matching the resolver's own first-accessible pre-selection.
85
87
  const displayedName = resolveDisplayedAgentName(primaryAgents, selectedAgent);
86
88
  const displayName = displayedName ? label(displayedName) : agentDisplayLabel(offered, selectedAgent);
87
- const metaSelected = isMetaAgentName(displayedName);
89
+ // The trigger leads with the agent's pixel mark, the same face it has on the
90
+ // Agents page and in every row below. Only for a real agent: a placeholder
91
+ // name such as a trigger's `default` would get a mark of its own, and that
92
+ // face would belong to no agent.
93
+ const triggerMark = displayedName && primaryAgents.some((agent) => agent.name === displayedName) ? (_jsx(AgentMark, { slug: displayedName })) : null;
94
+ const field = appearance === 'field';
95
+ const fieldTrigger = (caret, props) => (_jsxs("button", { type: "button", className: cn(triggerVariants(), 'h-9 w-full'), ...props, children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [triggerMark, _jsx("span", { className: "truncate", children: displayName })] }), caret ? _jsx(CaretDownIcon, { className: cn(TRIGGER_CARET_CLASS, 'size-4') }) : null] }));
88
96
  /**
89
97
  * One agent: name, its type, its description, and a check when it is the
90
98
  * one in use.
@@ -111,10 +119,7 @@ export function AgentSelector({ agents, selectedAgent, onSelect, disabled = fals
111
119
  className: cn('items-start gap-2 py-2', isSelected && 'bg-primary/[0.06]'), onSelect: () => {
112
120
  onSelect(agent.name);
113
121
  setOpen(false);
114
- }, children: [meta && (
115
- // Meta is the platform agent that runs the others, so its row leads
116
- // with its own mark instead of reading as one more peer in the list.
117
- _jsx("span", { className: "bg-muted flex size-8 shrink-0 items-center justify-center rounded-md", children: _jsx(SparkleIcon, { weight: "fill", className: "text-foreground size-4" }) })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx("span", { className: "text-foreground truncate text-sm font-medium", children: label(agent.name) }), meta && (_jsx(Badge, { variant: "outline", size: "xs", className: "shrink-0 font-normal", children: tHardcodedUi.raw('i18nComplete.textc78ffe195710') }))] }), agent.description && (_jsx("p", { className: "text-muted-foreground mt-0.5 truncate text-xs", children: agent.description }))] }), isSelected && (
122
+ }, children: [agent.description ? (_jsx(AgentMark, { slug: agent.name, size: 28, tile: 36 })) : (_jsx(AgentMark, { slug: agent.name, size: 14 })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx("span", { className: "text-foreground truncate text-sm font-medium", children: label(agent.name) }), meta && (_jsx(Badge, { variant: "outline", size: "xs", className: "shrink-0 font-normal", children: tHardcodedUi.raw('i18nComplete.textc78ffe195710') }))] }), agent.description && (_jsx("p", { className: "text-muted-foreground truncate text-xs", children: agent.description }))] }), isSelected && (
118
123
  // `mt-0.5` optically centres the 16px check against the first line
119
124
  // rather than the top edge of the text box.
120
125
  _jsx(CheckIcon, { className: "text-foreground mt-0.5 size-4 shrink-0" }))] }, agent.name));
@@ -135,7 +140,7 @@ export function AgentSelector({ agents, selectedAgent, onSelect, disabled = fals
135
140
  * open — in exactly the state where its words matter.
136
141
  */
137
142
  if (primaryAgents.length === 0) {
138
- const trigger = (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", disabled: true, "aria-label": unavailableHint ?? undefined, className: "text-muted-foreground rounded-lg hit-area-y-3", children: [_jsx("span", { className: cn('max-w-[100px] truncate', triggerLabelClassName), children: displayName }), _jsx(CaretDownIcon, { className: "size-3" })] }));
143
+ const trigger = field ? (fieldTrigger(false, { disabled: true, 'aria-label': unavailableHint ?? undefined })) : (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", disabled: true, "aria-label": unavailableHint ?? undefined, className: "text-muted-foreground rounded-lg hit-area-y-3", children: [_jsx("span", { className: cn('max-w-[100px] truncate', triggerLabelClassName), children: displayName }), _jsx(CaretDownIcon, { className: "size-3" })] }));
139
144
  if (!unavailableHint)
140
145
  return trigger;
141
146
  return (_jsx(Hint, { side: "top", label: unavailableHint, children: _jsx("span", { className: "inline-flex", children: trigger }) }));
@@ -148,7 +153,12 @@ export function AgentSelector({ agents, selectedAgent, onSelect, disabled = fals
148
153
  * text every inert control on this rail wears, and the disabled attribute.
149
154
  */
150
155
  if (disabled) {
151
- return (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", disabled: true, className: "text-muted-foreground rounded-lg hit-area-y-3", children: [metaSelected && _jsx(SparkleIcon, { className: "size-3.5 shrink-0", weight: "fill" }), _jsx("span", { className: cn('max-w-[100px] truncate', triggerLabelClassName), children: displayName })] }));
156
+ if (field)
157
+ return fieldTrigger(false, { disabled: true });
158
+ return (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", disabled: true, className: "text-muted-foreground rounded-lg hit-area-y-3", children: [triggerMark, _jsx("span", { className: cn('max-w-[100px] truncate', triggerLabelClassName), children: displayName })] }));
152
159
  }
153
- return (_jsxs(CommandPopover, { open: open, onOpenChange: setOpen, children: [_jsx(CommandPopoverTrigger, { children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": t('selectAgent'), className: "text-foreground/70 rounded-lg hit-area-y-3", children: [metaSelected && _jsx(SparkleIcon, { className: "size-3.5 shrink-0", weight: "fill" }), _jsx("span", { className: cn('max-w-[100px] truncate', triggerLabelClassName), children: displayName }), _jsx(CaretDownIcon, { className: cn('size-3 transition-transform duration-200 ease-out', open && 'rotate-180') })] }) }), _jsxs(CommandPopoverContent, { side: "top", align: "start", sideOffset: 8, className: "w-[min(300px,calc(100vw-1.5rem))]", children: [_jsx("div", { className: showSearch ? undefined : 'sr-only', children: _jsx(CommandInput, { compact: true, placeholder: tHardcodedUi.raw('componentsSessionSessionChatInput.line231JsxAttrPlaceholderSearchAgents'), value: search, onValueChange: setSearch }) }), _jsxs(CommandList, { className: "max-h-[320px]", children: [filteredPrimary.length > 0 && (_jsxs(CommandGroup, { forceMount: true, children: [filteredMeta.map((agent) => renderAgentItem(agent, true)), filteredMeta.length > 0 && filteredProject.length > 0 && (_jsx(CommandSeparator, { className: "my-1" })), filteredProject.map((agent) => renderAgentItem(agent, false))] })), filteredPrimary.length === 0 && search.trim() && (_jsxs("div", { className: "text-muted-foreground/50 py-8 text-center text-xs", children: [tHardcodedUi.raw('componentsSessionSessionChatInput.line273JsxTextNoAgentsMatchLdquo'), search.trim(), tHardcodedUi.raw('componentsSessionSessionChatInput.line273JsxTextRdquo')] }))] })] })] }));
160
+ return (_jsxs(CommandPopover, { open: open, onOpenChange: setOpen, children: [_jsx(CommandPopoverTrigger, { children: field ? (fieldTrigger(true, { 'aria-label': t('selectAgent') })) : (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": t('selectAgent'), className: "text-foreground/70 rounded-lg hit-area-y-3", children: [triggerMark, _jsx("span", { className: cn('max-w-[100px] truncate', triggerLabelClassName), children: displayName }), _jsx(CaretDownIcon, { className: cn('size-3 transition-transform duration-200 ease-out', open && 'rotate-180') })] })) }), _jsxs(CommandPopoverContent, { side: "top", align: "start", sideOffset: 8, className: "w-[min(300px,calc(100vw-1.5rem))]", children: [_jsx("div", { className: showSearch ? undefined : 'sr-only', children: _jsx(CommandInput, { compact: true, placeholder: tHardcodedUi.raw('componentsSessionSessionChatInput.line231JsxAttrPlaceholderSearchAgents'), value: search, onValueChange: setSearch }) }), _jsxs(CommandList, { className: "max-h-[320px]", children: [filteredPrimary.length > 0 && (
161
+ // `gap-px` between rows: a selected row and a hovered row next to it
162
+ // otherwise draw one merged grey block.
163
+ _jsxs(CommandGroup, { forceMount: true, className: "[&_[cmdk-group-items]]:flex [&_[cmdk-group-items]]:flex-col [&_[cmdk-group-items]]:gap-px", children: [filteredMeta.map((agent) => renderAgentItem(agent, true)), filteredMeta.length > 0 && filteredProject.length > 0 && (_jsx(CommandSeparator, { className: "my-1" })), filteredProject.map((agent) => renderAgentItem(agent, false))] })), filteredPrimary.length === 0 && search.trim() && (_jsxs("div", { className: "text-muted-foreground/50 py-8 text-center text-xs", children: [tHardcodedUi.raw('componentsSessionSessionChatInput.line273JsxTextNoAgentsMatchLdquo'), search.trim(), tHardcodedUi.raw('componentsSessionSessionChatInput.line273JsxTextRdquo')] }))] })] })] }));
154
164
  }
@@ -13,6 +13,11 @@ export interface ContextBreakdown {
13
13
  * compaction, whose usage is its summary reply's) with a positive token total.
14
14
  * `sessionLogThreadMetrics(...).context` picks the same message, but prices the
15
15
  * whole thread to do it.
16
+ *
17
+ * `reasoning` is a SUBSET of `output` (pi-ai `Usage`, and the same convention in
18
+ * OpenCode and the Kortix transcript): adding it again reads a session at 107%
19
+ * of a window the model has not filled, and hides the compaction that should
20
+ * have run. It stays in the breakdown for display, outside `total`.
16
21
  */
17
22
  export declare function getLastAssistantTokenBreakdown(messages: SessionLogMessage[] | undefined): ContextBreakdown;
18
23
  export declare function getLastAssistantTokenTotal(messages: SessionLogMessage[] | undefined): number;
@@ -10,6 +10,11 @@ const EMPTY_BREAKDOWN = {
10
10
  * compaction, whose usage is its summary reply's) with a positive token total.
11
11
  * `sessionLogThreadMetrics(...).context` picks the same message, but prices the
12
12
  * whole thread to do it.
13
+ *
14
+ * `reasoning` is a SUBSET of `output` (pi-ai `Usage`, and the same convention in
15
+ * OpenCode and the Kortix transcript): adding it again reads a session at 107%
16
+ * of a window the model has not filled, and hides the compaction that should
17
+ * have run. It stays in the breakdown for display, outside `total`.
13
18
  */
14
19
  export function getLastAssistantTokenBreakdown(messages) {
15
20
  if (!messages)
@@ -25,7 +30,7 @@ export function getLastAssistantTokenBreakdown(messages) {
25
30
  const output = t.output;
26
31
  const reasoning = t.reasoning ?? 0;
27
32
  const cache = t.cache_read + t.cache_write;
28
- const total = input + output + reasoning + cache;
33
+ const total = input + output + cache;
29
34
  if (total > 0)
30
35
  return { input, output, reasoning, cache, total };
31
36
  }
@@ -40,15 +40,15 @@ const HEADLINE_TIP_KEY = {
40
40
  };
41
41
  /**
42
42
  * Breakdown rows, in a fixed order chosen by size rather than by the order the
43
- * API happens to emit them: input dominates, cache is next, output and
44
- * reasoning are the tail. A fixed order matters more than a sorted one here —
45
- * rows must not reshuffle under the cursor as tokens stream in.
43
+ * API happens to emit them: input dominates, cache is next, output is the tail.
44
+ * A fixed order matters more than a sorted one here — rows must not reshuffle
45
+ * under the cursor as tokens stream in. `reasoning` is not a row: it is a subset
46
+ * of `output`, so counting it here would read context the model never sends.
46
47
  */
47
48
  const BREAKDOWN_ROWS = [
48
49
  { key: 'input', labelKey: 'labelInput' },
49
50
  { key: 'cache', labelKey: 'labelCached' },
50
51
  { key: 'output', labelKey: 'labelOutput' },
51
- { key: 'reasoning', labelKey: 'labelReasoning' },
52
52
  ];
53
53
  /**
54
54
  * Exported for the `/` palette: the "Show context" row's detail pane renders
@@ -9,8 +9,9 @@ interface TranscriptSessionHeader {
9
9
  }
10
10
  /**
11
11
  * The transcript's header: the runtime's session while the computer runs, else
12
- * the session row the page shows (asleep, stopped, failed). Null: nothing to
13
- * name the export after yet.
12
+ * the session row the page shows (asleep, stopped, failed). The start is the row's
13
+ * (`sessionStartedAt`): a pi box stamps its runtime session at boot. Null: nothing
14
+ * to name the export after yet.
14
15
  */
15
16
  export declare function transcriptSessionHeader(runtime: {
16
17
  id: string;
@@ -14,14 +14,17 @@ import { loadSessionExportMessages, useProjectSession, useRuntimeSession, useSes
14
14
  import { CheckIcon as Check, CopyIcon as Copy, } from '@phosphor-icons/react';
15
15
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
16
16
  import { Download } from '../../internal/icons/download.js';
17
+ import { sessionStartedAt } from '../thread/session-context-metrics.js';
17
18
  /**
18
19
  * The transcript's header: the runtime's session while the computer runs, else
19
- * the session row the page shows (asleep, stopped, failed). Null: nothing to
20
- * name the export after yet.
20
+ * the session row the page shows (asleep, stopped, failed). The start is the row's
21
+ * (`sessionStartedAt`): a pi box stamps its runtime session at boot. Null: nothing
22
+ * to name the export after yet.
21
23
  */
22
24
  export function transcriptSessionHeader(runtime, row, sessionId) {
23
25
  if (runtime) {
24
- return { id: runtime.id, title: runtime.title || runtime.slug || 'Untitled', time: runtime.time };
26
+ const created = sessionStartedAt(runtime, row) ?? runtime.time.created;
27
+ return { id: runtime.id, title: runtime.title || runtime.slug || 'Untitled', time: { ...runtime.time, created } };
25
28
  }
26
29
  if (!row)
27
30
  return null;
@@ -56,6 +56,8 @@ export interface ModelSelectorProps {
56
56
  triggerLabelClassName?: string;
57
57
  /** `secondary` for a page-level control; the composer keeps `ghost`. */
58
58
  triggerVariant?: 'ghost' | 'secondary';
59
+ /** `field`: the full-width `h-9` form-field box, the same as `AgentSelector`'s. */
60
+ appearance?: 'chip' | 'field';
59
61
  projectId?: string;
60
62
  /**
61
63
  * Offer the member's own ChatGPT subscription (pooled provider secrets) at
@@ -83,4 +85,4 @@ export interface ModelSelectorProps {
83
85
  open?: boolean;
84
86
  onOpenChange?: (open: boolean) => void;
85
87
  }
86
- export declare function ModelSelector({ models, selectedModel, onSelect, defaultControls, unsetLabel, disabled, modelsLoading, triggerLabelClassName, triggerVariant, offerProviderAccounts, open: openProp, onOpenChange, }: ModelSelectorProps): import("react").JSX.Element;
88
+ export declare function ModelSelector({ models, selectedModel, onSelect, defaultControls, unsetLabel, disabled, modelsLoading, triggerLabelClassName, triggerVariant, appearance, offerProviderAccounts, open: openProp, onOpenChange, }: ModelSelectorProps): import("react").JSX.Element;
@@ -5,6 +5,7 @@ import { CommandGroup, CommandInput, CommandItem, CommandList, CommandPopover, C
5
5
  import Loading from '../../primitives/loading.js';
6
6
  import { MODEL_SELECTOR_PROVIDER_IDS, ProviderLogo } from '../../internal/lib/provider-branding.js';
7
7
  import { cn } from '../../lib/utils.js';
8
+ import { TRIGGER_CARET_CLASS, triggerVariants } from '../../primitives/trigger-variants.js';
8
9
  import { useProviderModalStore } from '../../internal/stores/provider-modal-store.js';
9
10
  import { useFeatureFlag, useModelAccess, useModelStore, } from '@kortix/sdk/react';
10
11
  import { CheckIcon as Check, CaretDownIcon as ChevronDown, CaretRightIcon as CaretRight, CreditCardIcon as CreditCard, KeyIcon as KeyRound, PlusIcon as Plus, SlidersHorizontalIcon as SlidersHorizontal, StarIcon as Star, } from '@phosphor-icons/react';
@@ -136,7 +137,7 @@ function ModelRow({ model, groupProviderID, groupProviderName, isSelected, isAcc
136
137
  function GroupHeading({ children }) {
137
138
  return _jsx("span", { className: "text-muted-foreground font-medium tracking-wide", children: children });
138
139
  }
139
- export function ModelSelector({ models, selectedModel, onSelect, defaultControls, unsetLabel, disabled = false, modelsLoading = false, triggerLabelClassName, triggerVariant = 'ghost', offerProviderAccounts = false, open: openProp, onOpenChange, }) {
140
+ export function ModelSelector({ models, selectedModel, onSelect, defaultControls, unsetLabel, disabled = false, modelsLoading = false, triggerLabelClassName, triggerVariant = 'ghost', appearance = 'chip', offerProviderAccounts = false, open: openProp, onOpenChange, }) {
140
141
  const tModel = useTranslations('modelSelector');
141
142
  const t = useTranslations('threads');
142
143
  // Controlled when `open` is supplied, uncontrolled otherwise — Radix's own
@@ -303,7 +304,7 @@ export function ModelSelector({ models, selectedModel, onSelect, defaultControls
303
304
  setOpen(false);
304
305
  openUpgrade();
305
306
  }, [openUpgrade, setOpen]);
306
- return (_jsxs(_Fragment, { children: [connectionModal, projectId && chatGptPossible && ProviderAccountsModal && (_jsx(ProviderAccountsModal, { projectId: projectId, providerId: "codex", open: chatGptOpen, onOpenChange: setChatGptOpen })), _jsxs(CommandPopover, { open: disabled ? false : open, onOpenChange: (next) => !disabled && setOpen(next), children: [_jsx(CommandPopoverTrigger, { children: _jsxs(Button, { type: "button", variant: triggerVariant, size: "sm", className: cn('rounded-lg hit-area-y-3', triggerVariant === 'ghost' && 'text-muted-foreground'), children: [_jsx("span", { className: cn('max-w-30 truncate', triggerLabelClassName), children: displayName }), _jsx(ChevronDown, { className: cn('duration-moderate size-3 transition-transform ease-out', open && 'rotate-180') })] }) }), _jsx(CommandPopoverContent, { side: "top", align: "start", sideOffset: 8, className: "w-[min(300px,calc(100vw-1.5rem))]", children: _jsxs(_Fragment, { children: [_jsx(CommandInput, { compact: true, placeholder: tModel('search'), value: search, onValueChange: setSearch, rightElement: canManageModels && canConnectProvider ? (_jsxs("div", { className: "-mr-0.5 flex shrink-0 items-center gap-0.5", children: [_jsx(Hint, { label: tModel('connectProvider'), side: "top", className: "z-50", children: _jsx("button", { type: "button", "aria-label": tModel('addProvider'), onClick: () => handleOpenProviderModal('providers'), className: "text-muted-foreground hover:text-foreground hover:bg-muted flex size-8 cursor-pointer items-center justify-center rounded-md transition-colors", children: _jsx(Plus, { className: "size-4" }) }) }), _jsx(Hint, { label: tModel('manageModels'), side: "top", className: "z-50", children: _jsx("button", { type: "button", "aria-label": tModel('manageModels'), onClick: () => handleOpenProviderModal('models'), className: "text-muted-foreground hover:text-foreground hover:bg-muted flex size-8 cursor-pointer items-center justify-center rounded-md transition-colors", children: _jsx(SlidersHorizontal, { className: "size-4" }) }) })] })) : undefined }), grouped.length > 1 && _jsx(CommandSeparator, { className: "bg-border/60" }), _jsx(CommandList, { className: "max-h-[380px]", children: modelsLoading || entitlementsPending ? (_jsx("div", { className: "flex min-h-32 items-center justify-center", role: "status", "aria-label": tModel('loading'), children: _jsx(Loading, { className: "text-muted-foreground size-4 shrink-0" }) })) : grouped.length > 0 ? (_jsxs(_Fragment, { children: [pinnedDefault && (_jsxs(_Fragment, { children: [_jsx(CommandGroup, { heading: _jsx(GroupHeading, { children: tModel('yourDefault') }), forceMount: true, children: _jsx(ModelRow, { model: pinnedDefault.model, groupProviderID: pinnedDefault.providerID, groupProviderName: pinnedDefault.providerName, isSelected: availableSelectedModel?.providerID === pinnedDefault.model.providerID &&
307
+ return (_jsxs(_Fragment, { children: [connectionModal, projectId && chatGptPossible && ProviderAccountsModal && (_jsx(ProviderAccountsModal, { projectId: projectId, providerId: "codex", open: chatGptOpen, onOpenChange: setChatGptOpen })), _jsxs(CommandPopover, { open: disabled ? false : open, onOpenChange: (next) => !disabled && setOpen(next), children: [_jsx(CommandPopoverTrigger, { children: appearance === 'field' ? (_jsxs("button", { type: "button", disabled: disabled, className: cn(triggerVariants(), 'h-9 w-full'), children: [_jsx("span", { className: "truncate", children: displayName }), disabled ? null : _jsx(ChevronDown, { className: cn(TRIGGER_CARET_CLASS, 'size-4') })] })) : (_jsxs(Button, { type: "button", variant: triggerVariant, size: "sm", className: cn('rounded-lg hit-area-y-3', triggerVariant === 'ghost' && 'text-muted-foreground'), children: [_jsx("span", { className: cn('max-w-30 truncate', triggerLabelClassName), children: displayName }), _jsx(ChevronDown, { className: cn('duration-moderate size-3 transition-transform ease-out', open && 'rotate-180') })] })) }), _jsx(CommandPopoverContent, { side: "top", align: "start", sideOffset: 8, className: "w-[min(300px,calc(100vw-1.5rem))]", children: _jsxs(_Fragment, { children: [_jsx(CommandInput, { compact: true, placeholder: tModel('search'), value: search, onValueChange: setSearch, rightElement: canManageModels && canConnectProvider ? (_jsxs("div", { className: "-mr-0.5 flex shrink-0 items-center gap-0.5", children: [_jsx(Hint, { label: tModel('connectProvider'), side: "top", className: "z-50", children: _jsx("button", { type: "button", "aria-label": tModel('addProvider'), onClick: () => handleOpenProviderModal('providers'), className: "text-muted-foreground hover:text-foreground hover:bg-muted flex size-8 cursor-pointer items-center justify-center rounded-md transition-colors", children: _jsx(Plus, { className: "size-4" }) }) }), _jsx(Hint, { label: tModel('manageModels'), side: "top", className: "z-50", children: _jsx("button", { type: "button", "aria-label": tModel('manageModels'), onClick: () => handleOpenProviderModal('models'), className: "text-muted-foreground hover:text-foreground hover:bg-muted flex size-8 cursor-pointer items-center justify-center rounded-md transition-colors", children: _jsx(SlidersHorizontal, { className: "size-4" }) }) })] })) : undefined }), grouped.length > 1 && _jsx(CommandSeparator, { className: "bg-border/60" }), _jsx(CommandList, { className: "max-h-[380px]", children: modelsLoading || entitlementsPending ? (_jsx("div", { className: "flex min-h-32 items-center justify-center", role: "status", "aria-label": tModel('loading'), children: _jsx(Loading, { className: "text-muted-foreground size-4 shrink-0" }) })) : grouped.length > 0 ? (_jsxs(_Fragment, { children: [pinnedDefault && (_jsxs(_Fragment, { children: [_jsx(CommandGroup, { heading: _jsx(GroupHeading, { children: tModel('yourDefault') }), forceMount: true, children: _jsx(ModelRow, { model: pinnedDefault.model, groupProviderID: pinnedDefault.providerID, groupProviderName: pinnedDefault.providerName, isSelected: availableSelectedModel?.providerID === pinnedDefault.model.providerID &&
307
308
  availableSelectedModel?.modelID === pinnedDefault.model.modelID, isAccountDefault: true, defaultControls: rowDefaultControls, onSelect: handleSelect, connectLabel: tPooled('connect'), scope: "pinned" }) }), _jsx(CommandSeparator, {})] })), sections.map((section, sectionIndex) => {
308
309
  const selectedInSection = section.models.find((m) => availableSelectedModel?.providerID === m.providerID &&
309
310
  availableSelectedModel?.modelID === m.modelID);
@@ -10,11 +10,13 @@ import { type TodoItem } from '../../tool/shared/todo-helpers.js';
10
10
  * single row.
11
11
  *
12
12
  * Pure, so both branches are testable without mounting anything.
13
+ * @deprecated Renders an OpenCode-only tool or todo list in an OpenCode session (pi has none). OpenCode is sunset (2026-10-23): no new code; delete with OpenCode.
13
14
  */
14
15
  export declare function planPanelRows(todos: ReadonlyArray<TodoItem>): Array<{
15
16
  todo: TodoItem;
16
17
  key: string;
17
18
  }>;
19
+ /** @deprecated Renders an OpenCode-only tool or todo list in an OpenCode session (pi has none). OpenCode is sunset (2026-10-23): no new code; delete with OpenCode. */
18
20
  export declare function PlanPanelCard({ sessionId }: {
19
21
  sessionId: string;
20
22
  }): import("react").JSX.Element | null;
@@ -64,6 +64,7 @@ function PlanProgress({ done, total, running }) {
64
64
  * single row.
65
65
  *
66
66
  * Pure, so both branches are testable without mounting anything.
67
+ * @deprecated Renders an OpenCode-only tool or todo list in an OpenCode session (pi has none). OpenCode is sunset (2026-10-23): no new code; delete with OpenCode.
67
68
  */
68
69
  export function planPanelRows(todos) {
69
70
  // Keyed first, filtered second — see `planListTodos`: keying first is what
@@ -72,6 +73,7 @@ export function planPanelRows(todos) {
72
73
  const listed = planListTodos(keyed);
73
74
  return listed.length > 0 ? listed : keyed;
74
75
  }
76
+ /** @deprecated Renders an OpenCode-only tool or todo list in an OpenCode session (pi has none). OpenCode is sunset (2026-10-23): no new code; delete with OpenCode. */
75
77
  export function PlanPanelCard({ sessionId }) {
76
78
  const tI18nComplete = useTranslations('hardcodedUi.i18nComplete');
77
79
  const { data } = useRuntimeSessionTodo(sessionId);
@@ -5,5 +5,6 @@
5
5
  * `isActionPanelOpen` / `isSidePanelOpen` / `panelMode`: those made the plan
6
6
  * hop surfaces on every panel toggle, and they also re-rendered the whole
7
7
  * transcript each time one of them changed.
8
+ * @deprecated Renders an OpenCode-only tool or todo list in an OpenCode session (pi has none). OpenCode is sunset (2026-10-23): no new code; delete with OpenCode.
8
9
  */
9
10
  export declare function usePlanInChat(): boolean;
@@ -23,6 +23,7 @@ import { planBelongsToChat } from '../turn/plan-anchor.js';
23
23
  * `isActionPanelOpen` / `isSidePanelOpen` / `panelMode`: those made the plan
24
24
  * hop surfaces on every panel toggle, and they also re-rendered the whole
25
25
  * transcript each time one of them changed.
26
+ * @deprecated Renders an OpenCode-only tool or todo list in an OpenCode session (pi has none). OpenCode is sunset (2026-10-23): no new code; delete with OpenCode.
26
27
  */
27
28
  export function usePlanInChat() {
28
29
  return planBelongsToChat({ isMobile: useIsMobile() });
@@ -3435,7 +3435,7 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
3435
3435
  : welcomeWallpaper, !hideHeader && (_jsx(SessionSiteHeader, { sessionId: sessionId, sessionTitle: session?.title || 'Untitled', leadingAction: headerLeadingAction, parent: parentCrumb })), _jsx(SessionContextModal, { open: contextModalOpen, onOpenChange: setContextModalOpen,
3436
3436
  // A chat with no `useSession` source: the raw explorer lists the
3437
3437
  // rows `useSessionSync` read for it.
3438
- detachedRows: sessionState ? undefined : hookMessages, source: sessionState ?? DETACHED_SESSION_MESSAGES, session: session, providers: providers, allSessions: allSessions, servedModel: sessionServedModel, billedCost: sessionBilledCost(modelUsage) }), _jsx(CompactModal, { sessionId: sessionId, open: compactModalOpen, onOpenChange: setCompactModalOpen }), _jsx(ChangeRequestDetailDialog, { crId: openCrId, onClose: () => setOpenCrId(null), onOpenChangeRequest: setOpenCrId }), _jsxs(SessionBodyRow, { actionPanel: !hideHeader && !readOnly, children: [isNotFound ? (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col items-center justify-center gap-3 px-6 text-center", children: [_jsx("div", { className: "text-muted-foreground text-sm", children: tHardcodedUi.raw('componentsSessionSessionChat.line5821JsxTextThisSessionIsNotAccessibleRightNow') }), _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { href: "/", prefetch: true, onClick: () => {
3438
+ detachedRows: sessionState ? undefined : hookMessages, source: sessionState ?? DETACHED_SESSION_MESSAGES, session: session, sessionRow: projectSessionRow, providers: providers, allSessions: allSessions, servedModel: sessionServedModel, billedCost: sessionBilledCost(modelUsage) }), _jsx(CompactModal, { sessionId: sessionId, open: compactModalOpen, onOpenChange: setCompactModalOpen }), _jsx(ChangeRequestDetailDialog, { crId: openCrId, onClose: () => setOpenCrId(null), onOpenChangeRequest: setOpenCrId }), _jsxs(SessionBodyRow, { actionPanel: !hideHeader && !readOnly, children: [isNotFound ? (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col items-center justify-center gap-3 px-6 text-center", children: [_jsx("div", { className: "text-muted-foreground text-sm", children: tHardcodedUi.raw('componentsSessionSessionChat.line5821JsxTextThisSessionIsNotAccessibleRightNow') }), _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { href: "/", prefetch: true, onClick: () => {
3439
3439
  try {
3440
3440
  if (sessionId)
3441
3441
  useTabStore.getState().closeTab?.(sessionId);
@@ -3546,7 +3546,7 @@ export function SessionChat({ sessionId, projectSessionId, sessionState, project
3546
3546
  pendingTurnIds.has(turn.id) &&
3547
3547
  pendingTurnIds.has(turns[turnIndex - 1].id)
3548
3548
  ? 'mt-3'
3549
- : 'mt-12', turn: turn, author: turnAuthor, showAuthor: showAuthorName(turnAuthor, groupChat, viewerId), stoppedBy: stoppedByName(messageAuthors, turn.id, viewerId), servedModel: servedModelOfTurn(modelUsage, turn.id, turns[turnIndex - 1]?.id), turnOutcome: turnOutcome, isLast: turn.id === lastUserMessageId, ownsPlan: turn.id === planAnchorId, sessionId: sessionId, sessionStatus: sessionStatus, permissions: pendingPermissions, questions: pendingQuestions, agentNames: agentNames, isFirstTurn: turnIndex === 0,
3549
+ : 'mt-12', turn: turn, author: turnAuthor, showAuthor: showAuthorName(turnAuthor, groupChat, viewerId), stoppedBy: stoppedByName(messageAuthors, turn.id, viewerId), servedModel: servedModelOfTurn(modelUsage, turn.id, turns[turnIndex - 1]?.id), turnOutcome: turnOutcome, isLast: turn.id === lastUserMessageId, ownsPlan: turn.id === planAnchorId, sessionId: sessionId, projectId: projectId, projectSessionId: projectSessionId, sessionStatus: sessionStatus, permissions: pendingPermissions, questions: pendingQuestions, agentNames: agentNames, isFirstTurn: turnIndex === 0,
3550
3550
  // Handed over only once the stand-in has stepped
3551
3551
  // aside — while it is up it draws these itself.
3552
3552
  pendingText: turnIndex === 0 ? firstTurnHandover?.text : undefined, pendingAttachments: sentAttachmentsForTurn({
@@ -21,4 +21,19 @@ interface Metrics {
21
21
  }
22
22
  /** Pure token math for the modal — exported for characterization tests. */
23
23
  export declare function getSessionContextMetrics(thread: SessionLogThread | null, providers: ProviderListResponse | undefined, pricingLookup: ModelPricingLookup): Metrics;
24
+ /**
25
+ * When the session started. The session row's creation, when the runtime is the row's root (or
26
+ * not up): a pi box stamps its runtime session at boot, so a woken session's runtime time is after
27
+ * its last reply. A sub-agent's runtime (not the row's root) keeps its own time.
28
+ */
29
+ export declare function sessionStartedAt(runtime: {
30
+ id: string;
31
+ time: {
32
+ created: number;
33
+ };
34
+ } | undefined, row: {
35
+ created_at: string;
36
+ runtime_session_id?: string | null;
37
+ opencode_session_id?: string | null;
38
+ } | null | undefined): number | undefined;
24
39
  export {};
@@ -16,7 +16,7 @@ export function getSessionContextMetrics(thread, providers, pricingLookup) {
16
16
  ? model.provider
17
17
  : undefined;
18
18
  const limit = model?.limit?.context;
19
- const total = u.input + u.output + (u.reasoning ?? 0) + u.cache_read + u.cache_write;
19
+ const total = u.input + u.output + u.cache_read + u.cache_write; // reasoning is a subset of output
20
20
  // The gateway registers every model under the single synthetic `kortix`
21
21
  // opencode provider, so `provider.name` is always "Kortix" — even for a
22
22
  // BYOK Anthropic/Bedrock/OpenAI model. The gateway separately serves the
@@ -42,3 +42,12 @@ export function getSessionContextMetrics(thread, providers, pricingLookup) {
42
42
  },
43
43
  };
44
44
  }
45
+ /**
46
+ * When the session started. The session row's creation, when the runtime is the row's root (or
47
+ * not up): a pi box stamps its runtime session at boot, so a woken session's runtime time is after
48
+ * its last reply. A sub-agent's runtime (not the row's root) keeps its own time.
49
+ */
50
+ export function sessionStartedAt(runtime, row) {
51
+ const own = row && (!runtime || (row.runtime_session_id ?? row.opencode_session_id) === runtime.id);
52
+ return own ? Date.parse(row.created_at) : runtime?.time.created;
53
+ }