feat(connector-ui): say what the add-on declares for each file direction

The Files section showed only the limit in effect. It now also says,
for receive and send, whether the add-on declares a largest file, no
limit, or no file support, shows the host ceiling, and gives help text
on each field. A direction the add-on does not support is disabled.

Assisted-by: Claude:claude-sonnet-5-5 node
This commit is contained in:
Ettore Di Giacinto committed 2026-11-03 19:00:02 +00:00
1 parent e55227b4e8
commit 582642c964
3 files changed
+40 -12

No files matched your search

@@ -3,8 +3,8 @@
// limit of the connector kind. The page shows the limit in effect with the
// layer that sets it. The numbers come from the server; the words live here as
// defaults, like the rest of the shared fields.
import { Field, TextInput } from './controls'
import { fileLimitProblem, formatBytes, previewLimit } from './connector'
import { Field, Hint, TextInput } from './controls'
import { fileDirection, fileLimitProblem, formatBytes, previewLimit } from './connector'
export const FILES_TEXT = {
'files.title': 'Files',
@@ -14,6 +14,12 @@ export const FILES_TEXT = {
'files.invalid': 'Use a number greater than 0.',
'files.tooHigh': 'The add-on allows at most {{max}} here.',
'files.now': 'Now: {{limit}} ({{layer}}).',
'files.receiveHelp': 'Files that people send to the bot. A bigger file is refused, and the bot says which limit it hit.',
'files.sendHelp': 'Files that the agent makes and the bot sends back. A bigger file is not sent, and the bot says which limit it hit.',
'files.addon.off': 'This add-on does not move files in this direction, so there is nothing to set.',
'files.addon.declared': 'The add-on declares up to {{max}}.',
'files.addon.open': 'The add-on declares no limit, so the limit of the platform applies.',
'files.host': 'No bot can move a file above {{max}}. An administrator sets this in the settings.',
'files.layer.owner': 'limit set for this bot',
'files.layer.addon': 'limit of the add-on',
'files.layer.host': 'limit of the platform',
@@ -25,16 +31,19 @@ function shown(base, saved, mib) {
return previewLimit(base, mib) || (saved ? { bytes: saved.effective_bytes, layer: saved.layer } : null)
}
function Limit({ label, mib, onChange, base, saved, s, testId }) {
const problem = fileLimitProblem(mib, base?.allowed_bytes)
const limit = shown(base, saved, mib)
function Limit({ label, help, mib, onChange, base, saved, s, testId }) {
const dir = fileDirection(base)
const problem = dir === 'off' ? '' : fileLimitProblem(mib, base?.allowed_bytes)
const limit = dir === 'off' ? null : shown(base, saved, mib)
const error = problem === 'invalid' ? s('files.invalid') : problem === 'tooHigh' ? s('files.tooHigh', { max: formatBytes(base.allowed_bytes) }) : undefined
const declared = dir === 'declared' ? s('files.addon.declared', { max: formatBytes(base.declared_bytes) })
: dir === 'open' ? s('files.addon.open') : dir === 'off' ? s('files.addon.off') : ''
const now = limit ? s('files.now', { limit: formatBytes(limit.bytes), layer: s(`files.layer.${limit.layer}`) }) : ''
return (
<Field label={label} error={error}
hint={limit ? s('files.now', { limit: formatBytes(limit.bytes), layer: s(`files.layer.${limit.layer}`) }) : undefined}>
<Field label={label} error={error} hint={[help, declared, now].filter(Boolean).join(' ')}>
{(p) => (
<TextInput {...p} type="number" inputMode="decimal" min="0" step="any" data-testid={testId}
value={mib} onChange={(e) => onChange(e.target.value)} />
<TextInput {...p} type="number" inputMode="decimal" min="0" step="any" data-testid={testId} disabled={dir === 'off'}
value={dir === 'off' ? '' : mib} onChange={(e) => onChange(e.target.value)} />
)}
</Field>
)
@@ -49,13 +58,15 @@ function Limit({ label, mib, onChange, base, saved, s, testId }) {
* @param {Function} props.s the translation function with the defaults
*/
export default function FilesFields({ form, setForm, kind, saved, s }) {
const hostCeiling = kind?.host_ceiling_bytes || 0
return (
<fieldset className="cn-group" data-testid="connector-filelimits">
<legend>{s('files.title')}</legend>
<p className="dk-hint">{s('files.help')}</p>
<Limit label={s('files.receiveLabel')} mib={form.file_receive_mib} base={kind?.receive} saved={saved?.receive} s={s} testId="connector-file-receive"
{hostCeiling > 0 && <Hint>{s('files.host', { max: formatBytes(hostCeiling) })}</Hint>}
<Limit label={s('files.receiveLabel')} help={s('files.receiveHelp')} mib={form.file_receive_mib} base={kind?.receive} saved={saved?.receive} s={s} testId="connector-file-receive"
onChange={(v) => setForm({ ...form, file_receive_mib: v })} />
<Limit label={s('files.sendLabel')} mib={form.file_send_mib} base={kind?.send} saved={saved?.send} s={s} testId="connector-file-send"
<Limit label={s('files.sendLabel')} help={s('files.sendHelp')} mib={form.file_send_mib} base={kind?.send} saved={saved?.send} s={s} testId="connector-file-send"
onChange={(v) => setForm({ ...form, file_send_mib: v })} />
</fieldset>
)
@@ -172,6 +172,16 @@ export function previewLimit(base, mib) {
return { bytes: base.effective_bytes, layer: base.layer }
}
// What the add-on declares for one direction, from the limits of the kind:
// 'off' (it moves no file that way), 'declared' (it names a largest file),
// 'open' (it names none, so the platform limit applies) or 'unknown' (the page
// has no numbers yet).
export function fileDirection(base) {
if (!base) return 'unknown'
if (!(base.effective_bytes > 0) && !(base.allowed_bytes > 0)) return 'off'
return base.declared_bytes > 0 ? 'declared' : 'open'
}
// A size as a person reads it: bytes, KiB, MiB, GiB or TiB, one decimal at most.
export function formatBytes(n) {
if (n === 1) return '1 byte'
+8 -1
View File
@@ -1,6 +1,6 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { approvalForm, approvalPayload, approvalProblem, bytesToMib, canSave, connectorForm, connectorPayload, emptyConnector, fileLimitProblem, formatBytes, mibToBytes, modeText, previewLimit, senderLine } from '../../../internal/connector-ui/src/connector.js'
import { approvalForm, approvalPayload, approvalProblem, bytesToMib, canSave, connectorForm, connectorPayload, emptyConnector, fileDirection, fileLimitProblem, formatBytes, mibToBytes, modeText, previewLimit, senderLine } from '../../../internal/connector-ui/src/connector.js'
test('chat connectors: the token goes only when it was typed', () => {
const create = connectorPayload({ ...emptyConnector(), name: ' Bot ', token: ' 1:abc ', agent_id: 'a1', allow: ['11', ' 11', '@ada_l', ''] }, 'telegram')
@@ -121,6 +121,13 @@ test('chat connectors: a typed limit can only lower the limit in effect, and nam
assert.equal(previewLimit(null, '1'), null)
})
test('chat connectors: the page tells what the add-on declares for a direction', () => {
assert.equal(fileDirection(null), 'unknown')
assert.equal(fileDirection({ effective_bytes: 0, allowed_bytes: 0, declared_bytes: 0, layer: 'addon' }), 'off')
assert.equal(fileDirection({ effective_bytes: 100, allowed_bytes: 100, declared_bytes: 0, layer: 'host' }), 'open')
assert.equal(fileDirection({ effective_bytes: 50, allowed_bytes: 50, declared_bytes: 50, layer: 'addon' }), 'declared')
})
test('chat connectors: sizes are written in KiB and MiB', () => {
assert.equal(formatBytes(900), '900 bytes')
assert.equal(formatBytes(1), '1 byte')