@kortix/ui 0.14.14 → 0.14.15

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 (37) hide show
  1. package/dist/customize/agents/agent-tabs.d.ts +1 -0
  2. package/dist/customize/agents/agent-tabs.js +1 -0
  3. package/dist/customize/shared/capability-library.js +7 -2
  4. package/dist/files/drives/drive-storage.d.ts +47 -0
  5. package/dist/files/drives/drive-storage.js +144 -0
  6. package/dist/files/drives/session-drives-chip.js +20 -2
  7. package/dist/files/index.d.ts +1 -0
  8. package/dist/files/index.js +1 -0
  9. package/dist/files/project/components/drive-explorer.js +13 -5
  10. package/dist/files/project/components/drive-upload-tray.js +1 -1
  11. package/dist/files/project/upload-batch.d.ts +3 -1
  12. package/dist/files/project/upload-batch.js +5 -2
  13. package/dist/i18n/en.d.ts +32 -12
  14. package/dist/i18n/en.js +33 -13
  15. package/dist/internal/chrome/use-chrome-offered.d.ts +14 -5
  16. package/dist/internal/chrome/use-chrome-offered.js +15 -6
  17. package/dist/session/composer/context-usage.d.ts +2 -0
  18. package/dist/session/composer/context-usage.js +7 -0
  19. package/dist/session/header/compact-modal.js +5 -2
  20. package/dist/session/header/session-site-header.js +5 -2
  21. package/dist/session/thread/transcript.js +2 -2
  22. package/dist/session/tool/tools/kortix-tool-card.js +1 -1
  23. package/dist/session/turn/burst-step-body.js +2 -2
  24. package/dist/session/turn/compaction-card.d.ts +23 -1
  25. package/dist/session/turn/compaction-card.js +20 -8
  26. package/dist/styles.css +1 -1
  27. package/dist/workspace/sidebar/project-nav-items.d.ts +0 -11
  28. package/dist/workspace/sidebar/project-nav-items.js +10 -77
  29. package/dist/workspace/sidebar/project-sidebar.js +4 -3
  30. package/package.json +3 -7
  31. package/skills/kortix-ui/references/modules.md +1 -1
  32. package/dist/internal/stores/sidebar-nav-store.d.ts +0 -78
  33. package/dist/internal/stores/sidebar-nav-store.js +0 -125
  34. package/dist/workspace/sidebar/customize-sidebar-modal.d.ts +0 -9
  35. package/dist/workspace/sidebar/customize-sidebar-modal.js +0 -198
  36. package/dist/workspace/sidebar/nav-row-meta.d.ts +0 -14
  37. package/dist/workspace/sidebar/nav-row-meta.js +0 -94
@@ -66,6 +66,7 @@ export declare const YAML_KEY_ROW: {
66
66
  readonly options: "model";
67
67
  readonly tools: "tools";
68
68
  readonly codemode: "tools";
69
+ readonly context: "tools";
69
70
  readonly permission: "tools";
70
71
  readonly skills: "skills";
71
72
  readonly subagents: "subagents";
@@ -118,6 +118,7 @@ export const YAML_KEY_ROW = {
118
118
  options: 'model',
119
119
  tools: 'tools',
120
120
  codemode: 'tools',
121
+ context: 'tools',
121
122
  permission: 'tools',
122
123
  skills: 'skills',
123
124
  subagents: 'subagents',
@@ -101,8 +101,9 @@ function catalogRows(view, catalog, t, agentMode) {
101
101
  }
102
102
  const item = (ext) => ({
103
103
  id: ext.id,
104
- // "Follows the agent tool switch" only where that switch is on screen.
105
- description: ext.controlled_by && agentMode ? `${ext.description ?? ''} ${t('followsTool', { tool: ext.controlled_by })}` : ext.description,
104
+ // "Follows the agent tool switch" only where that switch is on screen. The context record's
105
+ // switch is its own row (the agent's `context` setting), so it follows nothing.
106
+ description: ext.controlled_by && ext.controlled_by !== 'context' && agentMode ? `${ext.description ?? ''} ${t('followsTool', { tool: ext.controlled_by })}` : ext.description,
106
107
  status: ext.required ? _jsx("span", { className: "text-muted-foreground text-xs", children: t('alwaysOn') }) : undefined,
107
108
  });
108
109
  // A required platform extension (permission rules on script calls, the show
@@ -140,6 +141,10 @@ function withSwitches(view, rows, catalog, editor) {
140
141
  const ext = catalog.extensions.find((x) => x.id === row.id);
141
142
  if (!ext || ext.required)
142
143
  return row;
144
+ // The context record is the agent's `context` field: on is the default (`record`), off is `summary`.
145
+ if (ext.group === 'platform' && ext.controlled_by === 'context') {
146
+ return { ...row, checked: editor.draft.context !== 'summary', onCheckedChange: (on) => editor.set('context', on ? 'record' : 'summary') };
147
+ }
143
148
  if (ext.group === 'platform') {
144
149
  const tool = catalog.tools.find((x) => x.id === ext.controlled_by);
145
150
  return tool ? { ...row, ...toolSwitch(tool.id, tool.aliases) } : row;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Files storage, shown where people put files: how much a project's Files
3
+ * hold against the plan's limit, the folders close to the largest folder a
4
+ * session mounts (before a session fails to mount one), and a refused upload
5
+ * in words with its numbers. Every figure comes from the API
6
+ * (`GET /drives/:driveId/usage`, `driveLimitRefusal`); nothing is computed
7
+ * from a guess here.
8
+ */
9
+ import { type DriveFolderUsage } from '@kortix/sdk';
10
+ import { useDriveUsage } from '@kortix/sdk/react';
11
+ export { useDriveUsage };
12
+ /**
13
+ * `5 GB`, `512 MB`, `3.4 KB` in the reader's locale. Binary steps (1 GB =
14
+ * 1024 MB), the same figures the API's refusals print; one decimal under 10.
15
+ */
16
+ export declare function formatStorageBytes(bytes: number, locale?: string): string;
17
+ /** `near` from 90 % of the limit, `full` at it. A storage limit is a policy a person can act on: orange, never red. */
18
+ export type StorageTone = 'ok' | 'near' | 'full';
19
+ export declare function storageTone(used: number, limit: number | null): StorageTone;
20
+ /** One headline figure against its limit: the label, `1.2 GB of 5 GB`, and one bar. */
21
+ export declare function StorageMeter({ label, usedBytes, limitBytes, className, }: {
22
+ label: string;
23
+ usedBytes: number;
24
+ limitBytes: number | null;
25
+ className?: string;
26
+ }): import("react").JSX.Element;
27
+ /** A folder at or past 80 % of the folder-mount cap, as one sentence. Null for a folder well under it. */
28
+ export declare function useFolderMountNotice(): (folder: DriveFolderUsage | undefined, limitBytes: number) => string | null;
29
+ /**
30
+ * The storage block of the Files page: the project's Files against the plan's
31
+ * limit (and the account's, when it is the closer one), what to do when it is
32
+ * nearly full, and each folder close to the folder-mount cap.
33
+ */
34
+ export declare function DriveStorageSummary({ driveId, className }: {
35
+ driveId: string;
36
+ className?: string;
37
+ }): import("react").JSX.Element | null;
38
+ /** A storage state a person can act on: the orange glyph and tint, the words in ink. */
39
+ export declare function StorageNotice({ children }: {
40
+ children: React.ReactNode;
41
+ }): import("react").JSX.Element | null;
42
+ /**
43
+ * The sentence for a refused upload, in the reader's language, from the
44
+ * refusal's code and numbers. Null for an error that is not a limit refusal:
45
+ * the caller keeps the server's message then.
46
+ */
47
+ export declare function useDriveLimitMessage(): (error: unknown, fileName: string, fileSize?: number) => string | null;
@@ -0,0 +1,144 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Files storage, shown where people put files: how much a project's Files
5
+ * hold against the plan's limit, the folders close to the largest folder a
6
+ * session mounts (before a session fails to mount one), and a refused upload
7
+ * in words with its numbers. Every figure comes from the API
8
+ * (`GET /drives/:driveId/usage`, `driveLimitRefusal`); nothing is computed
9
+ * from a guess here.
10
+ */
11
+ import { driveLimitRefusal } from '@kortix/sdk';
12
+ import { useDriveUsage } from '@kortix/sdk/react';
13
+ import { WarningIcon } from '@phosphor-icons/react';
14
+ import { useCallback } from 'react';
15
+ import { useLocale, useTranslations } from '../../i18n/index.js';
16
+ import { cn } from '../../lib/utils.js';
17
+ import { Progress } from '../../primitives/progress.js';
18
+ export { useDriveUsage };
19
+ const UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'];
20
+ /**
21
+ * `5 GB`, `512 MB`, `3.4 KB` in the reader's locale. Binary steps (1 GB =
22
+ * 1024 MB), the same figures the API's refusals print; one decimal under 10.
23
+ */
24
+ export function formatStorageBytes(bytes, locale) {
25
+ let value = Math.max(0, bytes);
26
+ let unit = 0;
27
+ while (value >= 1024 && unit < UNITS.length - 1) {
28
+ value /= 1024;
29
+ unit++;
30
+ }
31
+ return new Intl.NumberFormat(locale, {
32
+ style: 'unit',
33
+ unit: UNITS[unit],
34
+ unitDisplay: 'short',
35
+ maximumFractionDigits: unit === 0 || value >= 10 ? 0 : 1,
36
+ }).format(value);
37
+ }
38
+ export function storageTone(used, limit) {
39
+ if (!limit || limit <= 0)
40
+ return 'ok';
41
+ if (used >= limit)
42
+ return 'full';
43
+ return used >= limit * 0.9 ? 'near' : 'ok';
44
+ }
45
+ /** One headline figure against its limit: the label, `1.2 GB of 5 GB`, and one bar. */
46
+ export function StorageMeter({ label, usedBytes, limitBytes, className, }) {
47
+ const t = useTranslations('drives');
48
+ const locale = useLocale();
49
+ const tone = storageTone(usedBytes, limitBytes);
50
+ const used = formatStorageBytes(usedBytes, locale);
51
+ const percent = limitBytes ? Math.min(100, (usedBytes / limitBytes) * 100) : 0;
52
+ return (_jsxs("div", { className: cn('flex flex-col gap-1.5', className), children: [_jsxs("div", { className: "flex items-baseline justify-between gap-3", children: [_jsx("span", { className: "text-foreground min-w-0 truncate text-xs font-medium", children: label }), _jsx("span", { className: "text-muted-foreground shrink-0 text-xs tabular-nums", children: limitBytes ? t('storageOfLimit', { used, limit: formatStorageBytes(limitBytes, locale) }) : t('storageUsed', { used }) })] }), limitBytes ? (_jsx(Progress, { value: percent, "aria-label": label, className: "h-1.5", indicatorClassName: tone === 'ok' ? 'bg-primary' : 'bg-kortix-orange' })) : null] }));
53
+ }
54
+ /** A folder at or past 80 % of the folder-mount cap, as one sentence. Null for a folder well under it. */
55
+ export function useFolderMountNotice() {
56
+ const t = useTranslations('drives');
57
+ const locale = useLocale();
58
+ return useCallback((folder, limitBytes) => {
59
+ if (!folder || folder.state === 'ok')
60
+ return null;
61
+ const params = {
62
+ folder: folder.path.split('/').filter(Boolean).pop() ?? folder.path,
63
+ size: formatStorageBytes(folder.bytes, locale),
64
+ limit: formatStorageBytes(limitBytes, locale),
65
+ };
66
+ return folder.state === 'over_limit' ? t('folderOverMountLimit', params) : t('folderNearMountLimit', params);
67
+ }, [t, locale]);
68
+ }
69
+ /**
70
+ * The storage block of the Files page: the project's Files against the plan's
71
+ * limit (and the account's, when it is the closer one), what to do when it is
72
+ * nearly full, and each folder close to the folder-mount cap.
73
+ */
74
+ export function DriveStorageSummary({ driveId, className }) {
75
+ const t = useTranslations('drives');
76
+ const usage = useDriveUsage(driveId);
77
+ const folderNotice = useFolderMountNotice();
78
+ const data = usage.data;
79
+ if (!data || data.usedBytes === null)
80
+ return null;
81
+ const project = { used: data.usedBytes, limit: data.limits.projectBytes };
82
+ const account = data.account;
83
+ // The account figure shows only when it is the one closer to full.
84
+ const accountLeft = account ? account.limitBytes - account.usedBytes : Number.POSITIVE_INFINITY;
85
+ const projectLeft = project.limit === null ? Number.POSITIVE_INFINITY : project.limit - project.used;
86
+ const tightest = account && accountLeft < projectLeft ? storageTone(account.usedBytes, account.limitBytes) : storageTone(project.used, project.limit);
87
+ const folders = data.folders.filter((f) => f.state !== 'ok');
88
+ return (_jsxs("div", { className: cn('flex flex-col gap-2', className), children: [_jsx(StorageMeter, { label: t('storageLabel'), usedBytes: project.used, limitBytes: project.limit }), account && accountLeft < projectLeft ? (_jsx(AccountLine, { usedBytes: account.usedBytes, limitBytes: account.limitBytes })) : null, tightest !== 'ok' ? (_jsx(StorageNotice, { children: tightest === 'full' ? t('storageFull') : t('storageNearFull') })) : null, folders.map((f) => (_jsx(StorageNotice, { children: folderNotice(f, data.limits.folderMountBytes) }, f.path)))] }));
89
+ }
90
+ function AccountLine({ usedBytes, limitBytes }) {
91
+ const t = useTranslations('drives');
92
+ const locale = useLocale();
93
+ return (_jsx("p", { className: "text-muted-foreground text-xs tabular-nums", children: t('storageAccountLine', { used: formatStorageBytes(usedBytes, locale), limit: formatStorageBytes(limitBytes, locale) }) }));
94
+ }
95
+ /** A storage state a person can act on: the orange glyph and tint, the words in ink. */
96
+ export function StorageNotice({ children }) {
97
+ if (!children)
98
+ return null;
99
+ return (_jsxs("p", { role: "status", className: "bg-kortix-orange/15 text-foreground flex items-start gap-2 rounded-md px-2.5 py-2 text-xs text-pretty", children: [_jsx(WarningIcon, { className: "text-kortix-orange mt-px size-3.5 shrink-0", weight: "fill" }), _jsx("span", { className: "min-w-0", children: children })] }));
100
+ }
101
+ /**
102
+ * The sentence for a refused upload, in the reader's language, from the
103
+ * refusal's code and numbers. Null for an error that is not a limit refusal:
104
+ * the caller keeps the server's message then.
105
+ */
106
+ export function useDriveLimitMessage() {
107
+ const t = useTranslations('drives');
108
+ const locale = useLocale();
109
+ return useCallback((error, fileName, fileSize) => {
110
+ const refusal = driveLimitRefusal(error);
111
+ if (!refusal)
112
+ return null;
113
+ const bytes = (n) => (n === undefined ? '' : formatStorageBytes(n, locale));
114
+ const name = fileName;
115
+ switch (refusal.code) {
116
+ case 'storage_limit_exceeded':
117
+ if (refusal.max === undefined || refusal.used === undefined || (refusal.requested ?? fileSize) === undefined)
118
+ return refusal.message || null;
119
+ return t(refusal.limit === 'account_bytes' ? 'uploadRefusedAccountFull' : 'uploadRefusedProjectFull', {
120
+ name,
121
+ needed: bytes(refusal.requested ?? fileSize),
122
+ used: bytes(refusal.used),
123
+ limit: bytes(refusal.max),
124
+ });
125
+ case 'file_too_large':
126
+ case 'payload_too_large':
127
+ if (refusal.max === undefined)
128
+ return refusal.message || null;
129
+ return t('uploadRefusedFileTooLarge', { name, size: bytes(refusal.requested ?? fileSize), limit: bytes(refusal.max) });
130
+ case 'sub_path_too_large':
131
+ return t('uploadRefusedFolderMount', {
132
+ name,
133
+ folder: refusal.path?.split('/').filter(Boolean).pop() ?? refusal.path ?? '',
134
+ limit: bytes(refusal.max ?? 10 * 1024 ** 3),
135
+ });
136
+ case 'transfer_limit_exceeded':
137
+ return t('uploadRefusedBusy', { name });
138
+ case 'quota_exceeded':
139
+ return t('uploadRefusedStorageFull', { name });
140
+ default:
141
+ return null;
142
+ }
143
+ }, [t, locale]);
144
+ }
@@ -5,10 +5,20 @@ import { Button } from '../../primitives/button.js';
5
5
  import { warningToast } from '../../primitives/toast.js';
6
6
  import { Home } from '../../internal/icons/home.js';
7
7
  import { useSessionDrives } from './use-drives.js';
8
- import { useTranslations } from '../../i18n/index.js';
8
+ import { useLocale, useTranslations } from '../../i18n/index.js';
9
+ import { formatStorageBytes } from './drive-storage.js';
9
10
  import { ArrowUpRightIcon, FolderIcon, WarningIcon } from '@phosphor-icons/react';
10
11
  import { useUiLink } from '../../providers/ui-provider.js';
11
12
  import { useEffect, useRef } from 'react';
13
+ /**
14
+ * One notice per cause of a folder the box does not mount: storage refused it
15
+ * at boot (it mounts later), it is too large to mount, or it did not fit.
16
+ */
17
+ const SKIPPED_NOTICES = [
18
+ { key: 'sessionSkippedUnavailable', matches: (reason) => reason === 'unavailable' },
19
+ { key: 'sessionSkippedTooLarge', matches: (reason) => reason === 'too_large' },
20
+ { key: 'sessionSkippedDrives', matches: (reason) => reason !== 'unavailable' && reason !== 'too_large' },
21
+ ];
12
22
  /** Where the person's own folder also appears in the box: a path, not prose. */
13
23
  const DESKTOP_PATH = '~/Desktop';
14
24
  /**
@@ -22,12 +32,20 @@ export function SessionDrivesList({ projectId, sessionId }) {
22
32
  const t = useTranslations('drives');
23
33
  const Link = useUiLink();
24
34
  const driveHref = uiRoutes().drive(projectId);
35
+ const locale = useLocale();
25
36
  const session = useSessionDrives(projectId, sessionId);
26
37
  const rows = session.data?.drives ?? [];
27
38
  const skipped = session.data?.skipped ?? [];
39
+ // A folder too large to mount names its size: the number to get it under.
40
+ const named = (d) => d.reason === 'too_large' && typeof d.bytes === 'number' ? `${d.name} (${formatStorageBytes(d.bytes, locale)})` : d.name;
28
41
  return (_jsxs("div", { className: "flex flex-col gap-3", children: [driveHref ? (_jsx(Button, { asChild: true, variant: "ghost", size: "xs", className: "text-muted-foreground hover:text-foreground -ml-1.5 gap-1 self-start", children: _jsxs(Link, { href: driveHref, children: [t('openDrive'), _jsx(ArrowUpRightIcon, { className: "size-3.5" })] }) })) : null, rows.length === 0 ? (_jsx("p", { className: "text-muted-foreground py-2 text-xs", children: session.isLoading ? t('sessionCanUse') : t('sessionNoFolders') })) : (
29
42
  // Up to 8 mount (the sandbox's volume limit), so the list stays short.
30
- _jsx("ul", { className: "border-border divide-border divide-y rounded-lg border", children: rows.map((row) => (_jsx("li", { children: _jsx(SessionFolderRow, { driveHref: driveHref, row: row }) }, row.mountPath))) })), skipped.length > 0 ? (_jsxs("p", { role: "status", className: "bg-kortix-orange/15 text-foreground flex items-start gap-2 rounded-md px-2.5 py-2 text-xs text-pretty", children: [_jsx(WarningIcon, { className: "text-kortix-orange mt-px size-3.5 shrink-0", weight: "fill" }), t('sessionSkippedDrives', { count: skipped.length, names: skipped.map((d) => d.name).join(', ') })] })) : null] }));
43
+ _jsx("ul", { className: "border-border divide-border divide-y rounded-lg border", children: rows.map((row) => (_jsx("li", { children: _jsx(SessionFolderRow, { driveHref: driveHref, row: row }) }, row.mountPath))) })), SKIPPED_NOTICES.map(({ key, matches }) => {
44
+ const these = skipped.filter((d) => matches(d.reason));
45
+ if (!these.length)
46
+ return null;
47
+ return (_jsxs("p", { role: "status", className: "bg-kortix-orange/15 text-foreground flex items-start gap-2 rounded-md px-2.5 py-2 text-xs text-pretty", children: [_jsx(WarningIcon, { className: "text-kortix-orange mt-px size-3.5 shrink-0", weight: "fill" }), t(key, { count: these.length, names: these.map(named).join(', ') })] }, key));
48
+ })] }));
31
49
  }
32
50
  /**
33
51
  * Raises the session's conflict notice: one toast when a mounted folder gets a
@@ -4,6 +4,7 @@
4
4
  * session's workspace (`SandboxFileExplorer`, needs a mounted `useSession`).
5
5
  */
6
6
  export { saveDriveFile, useDismissDriveConflict, useDriveAvailability, useDriveConflicts, useDriveFolder, useFolderAccess, useFolderPrincipals, useInvalidateDrives, useProjectDrive, useShareFolder, useUnshareFolder, } from './drives/use-drives.js';
7
+ export { DriveStorageSummary, formatStorageBytes, StorageMeter, StorageNotice, storageTone, type StorageTone, useDriveLimitMessage, useDriveUsage, useFolderMountNotice, } from './drives/drive-storage.js';
7
8
  export { FilePreviewHost } from './file-preview-host.js';
8
9
  export { FilesStoreProvider, getFileIcon } from './project/index.js';
9
10
  export { ChangeRequestDetailDialog } from './project/components/change-request-detail-dialog.js';
@@ -4,6 +4,7 @@
4
4
  * session's workspace (`SandboxFileExplorer`, needs a mounted `useSession`).
5
5
  */
6
6
  export { saveDriveFile, useDismissDriveConflict, useDriveAvailability, useDriveConflicts, useDriveFolder, useFolderAccess, useFolderPrincipals, useInvalidateDrives, useProjectDrive, useShareFolder, useUnshareFolder, } from './drives/use-drives.js';
7
+ export { DriveStorageSummary, formatStorageBytes, StorageMeter, StorageNotice, storageTone, useDriveLimitMessage, useDriveUsage, useFolderMountNotice, } from './drives/drive-storage.js';
7
8
  export { FilePreviewHost } from './file-preview-host.js';
8
9
  export { FilesStoreProvider, getFileIcon } from './project/index.js';
9
10
  export { ChangeRequestDetailDialog } from './project/components/change-request-detail-dialog.js';
@@ -15,7 +15,8 @@ import { ErrorState } from '../../../primitives/error-state.js';
15
15
  import { isSandboxNotReadyError } from '@kortix/sdk';
16
16
  import { ClipboardIcon as Clipboard, FolderPlusIcon as FolderPlus, ClipboardTextIcon, MoonIcon, XIcon, UploadSimpleIcon, UploadIcon as Upload, ArrowsLeftRightIcon, CheckSquareIcon, TrashIcon, } from '@phosphor-icons/react';
17
17
  import { Download } from '../../../internal/icons/download.js';
18
- import { useTranslations } from '../../../i18n/index.js';
18
+ import { useLocale, useTranslations } from '../../../i18n/index.js';
19
+ import { formatStorageBytes } from '../../drives/drive-storage.js';
19
20
  import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
20
21
  import { useFileExplorerSource } from '../explorer-source.js';
21
22
  import { buildGitStatusMap } from '../hooks/index.js';
@@ -72,6 +73,13 @@ export function DriveExplorer({ embedded = false, shareContext, leading, trailin
72
73
  : source.capabilities;
73
74
  const canWrite = capabilities.write;
74
75
  const uploadLimit = capabilities.maxUploadBytes === undefined ? MAX_UPLOAD_BYTES : capabilities.maxUploadBytes;
76
+ const locale = useLocale();
77
+ // A file over the limit is refused before any byte moves, in the reader's language.
78
+ const describeRejection = useCallback((file, limit) => tDrives('uploadRefusedFileTooLarge', {
79
+ name: file.name,
80
+ size: formatStorageBytes(file.size, locale),
81
+ limit: formatStorageBytes(limit, locale),
82
+ }), [tDrives, locale]);
75
83
  const currentPath = useFilesStore((s) => s.currentPath);
76
84
  const navigateToPath = useFilesStore((s) => s.navigateToPath);
77
85
  const viewMode = useFilesStore((s) => s.viewMode);
@@ -462,7 +470,7 @@ export function DriveExplorer({ embedded = false, shareContext, leading, trailin
462
470
  * One failed file never stops the rest; its tray row offers Retry.
463
471
  */
464
472
  const handleUploadFiles = useCallback(async (fileList, targetDirPath) => {
465
- const { accepted, rejected } = partitionUploadBatch(Array.from(fileList), uploadLimit);
473
+ const { accepted, rejected } = partitionUploadBatch(Array.from(fileList), uploadLimit, describeRejection);
466
474
  for (const { reason } of rejected)
467
475
  errorToast(reason);
468
476
  if (accepted.length === 0)
@@ -481,14 +489,14 @@ export function DriveExplorer({ embedded = false, shareContext, leading, trailin
481
489
  ]);
482
490
  }
483
491
  await startBatch(accepted.map((file) => ({ file, targetPath })), shownIn, targetDirPath ?? currentPath);
484
- }, [currentPath, isRootPath, pendingNode, startBatch, uploadLimit]);
492
+ }, [currentPath, isRootPath, pendingNode, startBatch, uploadLimit, describeRejection]);
485
493
  /**
486
494
  * Upload a tree (a picked or dropped folder) into `base`: its folders are
487
495
  * made first, one depth at a time (a level in parallel), then its files go
488
496
  * up into them. Top folders landing in the folder in view show at once.
489
497
  */
490
498
  const uploadTree = useCallback(async (tree, base) => {
491
- const { accepted, rejected } = partitionUploadBatch(tree.map((t) => t.file), uploadLimit);
499
+ const { accepted, rejected } = partitionUploadBatch(tree.map((t) => t.file), uploadLimit, describeRejection);
492
500
  for (const { reason } of rejected)
493
501
  errorToast(reason);
494
502
  const acceptedSet = new Set(accepted);
@@ -530,7 +538,7 @@ export function DriveExplorer({ embedded = false, shareContext, leading, trailin
530
538
  await startBatch(items.map(({ file, dir: rel }) => ({ file, targetPath: at(rel) || undefined })),
531
539
  // Loose files dropped beside the folders land in the view.
532
540
  inView ? currentPath : null, tops.length === 1 ? at(tops[0]) : root || '/');
533
- }, [currentPath, normalizedCurrentPath, pendingNode, settlePending, mkdirMutation, startBatch, tHardcodedUi, uploadLimit]);
541
+ }, [currentPath, normalizedCurrentPath, pendingNode, settlePending, mkdirMutation, startBatch, tHardcodedUi, uploadLimit, describeRejection]);
534
542
  /** The folder picker (`webkitRelativePath` is `Picked/sub/file.png`). The browser already asked. */
535
543
  const handleUploadFolder = useCallback((fileList) => uploadTree(Array.from(fileList).map((file) => ({
536
544
  file,
@@ -57,7 +57,7 @@ export function DriveUploadTray() {
57
57
  : failed > 0
58
58
  ? t('uploadsFailedCount', { count: failed })
59
59
  : t('uploadsComplete', { count: done });
60
- return (_jsxs("section", { "aria-label": title, className: "bg-popover border-border animate-in fade-in-0 slide-in-from-bottom-2 fixed right-4 bottom-4 z-50 w-80 max-w-[calc(100vw-2rem)] overflow-hidden rounded-md border shadow-lg duration-(--duration-moderate) motion-reduce:animate-none", children: [_jsxs("div", { className: "flex h-10 items-center gap-2 pr-1.5 pl-3", children: [active > 0 ? (_jsx(UploadGauge, { value: fraction, className: "size-5" })) : failed > 0 ? (_jsx(WarningCircleIcon, { className: "text-kortix-red size-5 shrink-0" })) : (_jsx(SolidCheckIcon, { className: "text-kortix-green size-5" })), _jsx("p", { "aria-live": "polite", className: "min-w-0 flex-1 truncate text-sm font-medium tabular-nums", children: title }), active > 0 ? (_jsxs("span", { className: "text-muted-foreground shrink-0 text-xs tabular-nums", children: [Math.min(99, Math.round(fraction * 100)), "%"] })) : null, _jsx(Button, { type: "button", variant: "ghost", size: "icon-xs", "aria-label": t('uploadsToggle'), "aria-expanded": !collapsed, onClick: () => setCollapsed((c) => !c), className: "text-muted-foreground hover:text-foreground", children: _jsx(CaretDownIcon, { className: cn('size-3.5 transition-transform duration-(--duration-moderate) motion-reduce:transition-none', collapsed && 'rotate-180') }) }), active === 0 ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon-xs", "aria-label": t('uploadsDismiss'), onClick: clear, className: "text-muted-foreground hover:text-foreground", children: _jsx(XIcon, { className: "size-3.5" }) })) : null] }), !collapsed ? (_jsx("ul", { className: "border-border/60 max-h-64 overflow-y-auto border-t py-1", children: uploads.map((u) => (_jsxs("li", { className: "flex h-9 items-center gap-2.5 px-3", children: [getFileIcon(u.file.name, { className: 'text-muted-foreground size-4 shrink-0' }), _jsx("span", { className: cn('min-w-0 flex-1 truncate text-sm', u.status === 'done' ? 'text-foreground' : 'text-muted-foreground'), title: u.file.name, children: u.file.name }), _jsx(UploadStatus, { upload: u, onRetry: () => u.retry?.() })] }, u.id))) })) : null] }));
60
+ return (_jsxs("section", { "aria-label": title, className: "bg-popover border-border animate-in fade-in-0 slide-in-from-bottom-2 fixed right-4 bottom-4 z-50 w-80 max-w-[calc(100vw-2rem)] overflow-hidden rounded-md border shadow-lg duration-(--duration-moderate) motion-reduce:animate-none", children: [_jsxs("div", { className: "flex h-10 items-center gap-2 pr-1.5 pl-3", children: [active > 0 ? (_jsx(UploadGauge, { value: fraction, className: "size-5" })) : failed > 0 ? (_jsx(WarningCircleIcon, { className: "text-kortix-red size-5 shrink-0" })) : (_jsx(SolidCheckIcon, { className: "text-kortix-green size-5" })), _jsx("p", { "aria-live": "polite", className: "min-w-0 flex-1 truncate text-sm font-medium tabular-nums", children: title }), active > 0 ? (_jsxs("span", { className: "text-muted-foreground shrink-0 text-xs tabular-nums", children: [Math.min(99, Math.round(fraction * 100)), "%"] })) : null, _jsx(Button, { type: "button", variant: "ghost", size: "icon-xs", "aria-label": t('uploadsToggle'), "aria-expanded": !collapsed, onClick: () => setCollapsed((c) => !c), className: "text-muted-foreground hover:text-foreground", children: _jsx(CaretDownIcon, { className: cn('size-3.5 transition-transform duration-(--duration-moderate) motion-reduce:transition-none', collapsed && 'rotate-180') }) }), active === 0 ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon-xs", "aria-label": t('uploadsDismiss'), onClick: clear, className: "text-muted-foreground hover:text-foreground", children: _jsx(XIcon, { className: "size-3.5" }) })) : null] }), !collapsed ? (_jsx("ul", { className: "border-border/60 max-h-64 overflow-y-auto border-t py-1", children: uploads.map((u) => (_jsxs("li", { className: "flex min-h-9 items-center gap-2.5 px-3 py-1.5", children: [getFileIcon(u.file.name, { className: 'text-muted-foreground size-4 shrink-0' }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: cn('truncate text-sm', u.status === 'done' ? 'text-foreground' : 'text-muted-foreground'), title: u.file.name, children: u.file.name }), u.status === 'error' && u.error ? (_jsx("span", { className: "text-foreground text-xs text-pretty", children: u.error })) : null] }), _jsx(UploadStatus, { upload: u, onRetry: () => u.retry?.() })] }, u.id))) })) : null] }));
61
61
  }
62
62
  function UploadStatus({ upload, onRetry }) {
63
63
  const t = useTranslations('drives');
@@ -40,7 +40,9 @@ export interface UploadBatchPartition<T extends UploadCandidate> {
40
40
  */
41
41
  export declare function uploadRejectionReason(file: UploadCandidate, limit?: number | null): string | null;
42
42
  /** Split a batch into what will be sent and what is refused up front. `limit` null takes any size. */
43
- export declare function partitionUploadBatch<T extends UploadCandidate>(files: readonly T[], limit?: number | null): UploadBatchPartition<T>;
43
+ export declare function partitionUploadBatch<T extends UploadCandidate>(files: readonly T[], limit?: number | null,
44
+ /** The sentence for a refused file, in the reader's language; the English default otherwise. */
45
+ describe?: (file: T, limit: number) => string): UploadBatchPartition<T>;
44
46
  /**
45
47
  * Share of the run sent, 0-1, by bytes: a finished file counts whole, a failed
46
48
  * one drops out. An all-empty batch falls back to counting files.
@@ -32,11 +32,14 @@ export function uploadRejectionReason(file, limit = MAX_UPLOAD_BYTES) {
32
32
  return `${file.name} is ${formatFileSize(file.size)} — the upload limit is ${label}`;
33
33
  }
34
34
  /** Split a batch into what will be sent and what is refused up front. `limit` null takes any size. */
35
- export function partitionUploadBatch(files, limit = MAX_UPLOAD_BYTES) {
35
+ export function partitionUploadBatch(files, limit = MAX_UPLOAD_BYTES,
36
+ /** The sentence for a refused file, in the reader's language; the English default otherwise. */
37
+ describe) {
36
38
  const accepted = [];
37
39
  const rejected = [];
38
40
  for (const file of files) {
39
- const reason = uploadRejectionReason(file, limit);
41
+ const english = uploadRejectionReason(file, limit);
42
+ const reason = english && describe && limit !== null ? describe(file, limit) : english;
40
43
  if (reason)
41
44
  rejected.push({ file, reason });
42
45
  else
package/dist/i18n/en.d.ts CHANGED
@@ -906,6 +906,7 @@ declare const catalog: {
906
906
  addCredits: string;
907
907
  spentThisPeriod: string;
908
908
  compute: string;
909
+ storage: string;
909
910
  total: string;
910
911
  history: string;
911
912
  historyDescription: string;
@@ -1337,20 +1338,8 @@ declare const catalog: {
1337
1338
  };
1338
1339
  drive: string;
1339
1340
  repo: string;
1340
- customizeSidebar: string;
1341
1341
  searchApps: string;
1342
1342
  appsNoMatches: string;
1343
- customizeSidebarDescription: string;
1344
- resetSidebar: string;
1345
- reorderItem: string;
1346
- reorderHint: string;
1347
- reorderAnnouncement: string;
1348
- more: string;
1349
- inSidebar: string;
1350
- underMore: string;
1351
- sidebarEmpty: string;
1352
- moreEmpty: string;
1353
- moreCaption: string;
1354
1343
  spaces: {
1355
1344
  channels: string;
1356
1345
  directMessages: string;
@@ -7448,6 +7437,8 @@ declare const catalog: {
7448
7437
  agentFullWriteDescription: string;
7449
7438
  agentCardDrive: string;
7450
7439
  sessionSkippedDrives: string;
7440
+ sessionSkippedUnavailable: string;
7441
+ sessionSkippedTooLarge: string;
7451
7442
  shareFolder: string;
7452
7443
  shareTitle: string;
7453
7444
  shareDescription: string;
@@ -7515,6 +7506,24 @@ declare const catalog: {
7515
7506
  readWrite: string;
7516
7507
  folderHint: string;
7517
7508
  driveLabel: string;
7509
+ storageLabel: string;
7510
+ storageOfLimit: string;
7511
+ storageUsed: string;
7512
+ storageNearFull: string;
7513
+ storageFull: string;
7514
+ storageAccountLine: string;
7515
+ folderNearMountLimit: string;
7516
+ folderOverMountLimit: string;
7517
+ uploadRefusedProjectFull: string;
7518
+ uploadRefusedAccountFull: string;
7519
+ uploadRefusedFileTooLarge: string;
7520
+ uploadRefusedFolderMount: string;
7521
+ uploadRefusedBusy: string;
7522
+ uploadRefusedStorageFull: string;
7523
+ accountStorageTitle: string;
7524
+ accountStorageDescription: string;
7525
+ accountStorageEmpty: string;
7526
+ accountStorageIncomplete: string;
7518
7527
  };
7519
7528
  genui: {
7520
7529
  showData: string;
@@ -9571,5 +9580,16 @@ declare const catalog: {
9571
9580
  connectClaude: string;
9572
9581
  reconnectClaude: string;
9573
9582
  };
9583
+ contextReset: {
9584
+ pill: string;
9585
+ pillRunning: string;
9586
+ detail: string;
9587
+ detailManual: string;
9588
+ showDetail: string;
9589
+ hideDetail: string;
9590
+ dialogTitle: string;
9591
+ dialogDescription: string;
9592
+ dialogAction: string;
9593
+ };
9574
9594
  };
9575
9595
  export default catalog;
package/dist/i18n/en.js CHANGED
@@ -907,6 +907,7 @@ const catalog = {
907
907
  "addCredits": "Add credits",
908
908
  "spentThisPeriod": "Spent this period",
909
909
  "compute": "Compute",
910
+ "storage": "Storage",
910
911
  "total": "Total",
911
912
  "history": "History",
912
913
  "historyDescription": "What every session cost, and every credit added or spent.",
@@ -1338,20 +1339,8 @@ const catalog = {
1338
1339
  },
1339
1340
  "drive": "Drive",
1340
1341
  "repo": "Repo",
1341
- "customizeSidebar": "Customize sidebar",
1342
1342
  "searchApps": "Search apps",
1343
1343
  "appsNoMatches": "No matches",
1344
- "customizeSidebarDescription": "Drag rows between the two groups.",
1345
- "resetSidebar": "Reset to default",
1346
- "reorderItem": "Reorder {name}",
1347
- "reorderHint": "Drag to reorder. Or press the up and down arrow keys; moving past More hides the row from the sidebar.",
1348
- "reorderAnnouncement": "{name}: {group}, position {position} of {total}",
1349
- "more": "More",
1350
- "inSidebar": "Sidebar",
1351
- "underMore": "More",
1352
- "sidebarEmpty": "Drag a row here to show it in the sidebar.",
1353
- "moreEmpty": "Drag a row here to move it under More.",
1354
- "moreCaption": "Rows under More open from the More menu, below the nav.",
1355
1344
  "spaces": {
1356
1345
  "channels": "Channels",
1357
1346
  "directMessages": "Direct messages",
@@ -7449,6 +7438,8 @@ const catalog = {
7449
7438
  "agentFullWriteDescription": "In the sessions you start, this agent can change any file in your drive, not only its From agents folder.",
7450
7439
  "agentCardDrive": "{size} drive",
7451
7440
  "sessionSkippedDrives": "{count, plural, one {# drive did not fit in this session} other {# drives did not fit in this session}}: {names}. Take a drive out to make room, then restart the session.",
7441
+ "sessionSkippedUnavailable": "{count, plural, one {# folder is not mounted yet} other {# folders are not mounted yet}}: {names}. The session runs without {count, plural, one {it} other {them}} and mounts {count, plural, one {it} other {them}} as soon as storage can.",
7442
+ "sessionSkippedTooLarge": "{count, plural, one {# folder is too large to mount in a session} other {# folders are too large to mount in a session}}: {names}. The session runs without {count, plural, one {it} other {them}}. Move large files out, or share a smaller folder.",
7452
7443
  "shareFolder": "Share",
7453
7444
  "shareTitle": "Share {name}",
7454
7445
  "shareDescription": "People, teams and agents you add get this folder and everything in it. A session sees a folder when its agent, its project or the person who started it has access.",
@@ -7515,7 +7506,25 @@ const catalog = {
7515
7506
  "sessionFilesDescription": "The Drive folders this session mounts, and where the agent sees them. Share a folder in Drive to add it; stop sharing to take it away.",
7516
7507
  "readWrite": "Read & write",
7517
7508
  "folderHint": "A folder inside {name}",
7518
- "driveLabel": "Drive"
7509
+ "driveLabel": "Drive",
7510
+ "storageLabel": "Storage",
7511
+ "storageOfLimit": "{used} of {limit}",
7512
+ "storageUsed": "{used} used",
7513
+ "storageNearFull": "Almost full. Delete files you no longer need to keep uploading.",
7514
+ "storageFull": "Full. New uploads are refused until files are deleted.",
7515
+ "storageAccountLine": "All projects: {used} of {limit}",
7516
+ "folderNearMountLimit": "{folder} holds {size}. A session cannot mount a folder larger than {limit}.",
7517
+ "folderOverMountLimit": "{folder} holds {size}, more than the {limit} a session can mount. Sessions run without it.",
7518
+ "uploadRefusedProjectFull": "{name} needs {needed}. This project's Files use {used} of {limit}.",
7519
+ "uploadRefusedAccountFull": "{name} needs {needed}. Your account's Files use {used} of {limit} across all projects.",
7520
+ "uploadRefusedFileTooLarge": "{name} is {size}. One file can be at most {limit}.",
7521
+ "uploadRefusedFolderMount": "{name} would take {folder} past {limit}, the largest folder a session can mount.",
7522
+ "uploadRefusedBusy": "File storage is busy. Retry {name} in a moment.",
7523
+ "uploadRefusedStorageFull": "File storage is full right now. Retry {name} later.",
7524
+ "accountStorageTitle": "Files storage",
7525
+ "accountStorageDescription": "What each project's Files hold. Your plan allows {project} per project and {account} across all projects.",
7526
+ "accountStorageEmpty": "No project holds files yet.",
7527
+ "accountStorageIncomplete": "Some projects could not be measured right now."
7519
7528
  },
7520
7529
  "genui": {
7521
7530
  "showData": "Show data",
@@ -9571,6 +9580,17 @@ const catalog = {
9571
9580
  "codeError": "Could not connect Claude with that code. Sign in again and paste the new code.",
9572
9581
  "connectClaude": "Connect Claude",
9573
9582
  "reconnectClaude": "Reconnect Claude"
9583
+ },
9584
+ "contextReset": {
9585
+ "pill": "Fresh context · full history kept",
9586
+ "pillRunning": "Freeing up context…",
9587
+ "detail": "The context reached {tokens} tokens, so the agent continued in a fresh one. Nothing was summarized: it searches the full history ({entries} entries) when it needs a detail.",
9588
+ "detailManual": "You started a fresh context at {tokens} tokens. Nothing was summarized: the agent searches the full history ({entries} entries) when it needs a detail.",
9589
+ "showDetail": "Show details",
9590
+ "hideDetail": "Hide details",
9591
+ "dialogTitle": "Start a fresh context?",
9592
+ "dialogDescription": "The agent continues in a fresh context and searches the full history when it needs a detail. Nothing is summarized, and the conversation stays here as it is.",
9593
+ "dialogAction": "Start a fresh context"
9574
9594
  }
9575
9595
  };
9576
9596
  export default catalog;
@@ -1,10 +1,19 @@
1
1
  /** @internal Kortix apps only. No semver: it may change in any release. */
2
2
  /**
3
- * Whether this platform offers Kortix in Chrome at all. Until launch the API
4
- * answers 404 on /v1/chrome/* where KORTIX_CHROME_ENABLED is off (staging,
5
- * prod), so the account-level surfaces — Settings → Kortix in Chrome and its
6
- * command-palette entry — show only once the browser list answers. Project
7
- * surfaces read the project's `chrome` flag instead.
3
+ * The web app's master switch for Kortix in Chrome. Off, no surface renders
4
+ * and nothing calls /v1/chrome/*: no Settings page, no command-palette entry,
5
+ * no sidebar row and no card in Apps and extensions. Off on every deployment
6
+ * until launch: the probe below logged a 404 in the console on staging and
7
+ * prod, where the API's KORTIX_CHROME_ENABLED is off. Set true to bring the
8
+ * surfaces back where the API serves the routes.
9
+ */
10
+ export declare const CHROME_UI_ENABLED = false;
11
+ /**
12
+ * Whether this platform offers Kortix in Chrome at all. The API answers 404
13
+ * on /v1/chrome/* where KORTIX_CHROME_ENABLED is off (staging, prod), so the
14
+ * account-level surfaces — Settings → Kortix in Chrome and its command-palette
15
+ * entry — show only once the browser list answers. Project surfaces read the
16
+ * project's `chrome` flag and `CHROME_UI_ENABLED` instead.
8
17
  *
9
18
  * `enabled`: ask only while the surface that needs the answer is open.
10
19
  */
@@ -3,11 +3,20 @@ import { listChromeConnections } from '@kortix/sdk';
3
3
  import { chromeKeys } from '@kortix/sdk/react';
4
4
  import { useQuery } from '@tanstack/react-query';
5
5
  /**
6
- * Whether this platform offers Kortix in Chrome at all. Until launch the API
7
- * answers 404 on /v1/chrome/* where KORTIX_CHROME_ENABLED is off (staging,
8
- * prod), so the account-level surfaces — Settings → Kortix in Chrome and its
9
- * command-palette entry — show only once the browser list answers. Project
10
- * surfaces read the project's `chrome` flag instead.
6
+ * The web app's master switch for Kortix in Chrome. Off, no surface renders
7
+ * and nothing calls /v1/chrome/*: no Settings page, no command-palette entry,
8
+ * no sidebar row and no card in Apps and extensions. Off on every deployment
9
+ * until launch: the probe below logged a 404 in the console on staging and
10
+ * prod, where the API's KORTIX_CHROME_ENABLED is off. Set true to bring the
11
+ * surfaces back where the API serves the routes.
12
+ */
13
+ export const CHROME_UI_ENABLED = false;
14
+ /**
15
+ * Whether this platform offers Kortix in Chrome at all. The API answers 404
16
+ * on /v1/chrome/* where KORTIX_CHROME_ENABLED is off (staging, prod), so the
17
+ * account-level surfaces — Settings → Kortix in Chrome and its command-palette
18
+ * entry — show only once the browser list answers. Project surfaces read the
19
+ * project's `chrome` flag and `CHROME_UI_ENABLED` instead.
11
20
  *
12
21
  * `enabled`: ask only while the surface that needs the answer is open.
13
22
  */
@@ -15,7 +24,7 @@ export function useChromeOffered(enabled = true) {
15
24
  return useQuery({
16
25
  queryKey: chromeKeys.list(),
17
26
  queryFn: listChromeConnections,
18
- enabled,
27
+ enabled: CHROME_UI_ENABLED && enabled,
19
28
  staleTime: 60_000,
20
29
  retry: false,
21
30
  }).isSuccess;