dsh-hypatia-auto-memory 2.1.1 → 2.2.1

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.
@@ -2,12 +2,17 @@
2
2
  * Configuration card for the `hypatia-auto-memory` namespace, rendered on
3
3
  * whichever seat the harness offers (see `plugin-card-seat.ts`).
4
4
  *
5
- * The card owns its disclosure state and stages edits until Save. Its visual
6
- * treatment mirrors the DSH plugin-settings cards while remaining bundle-local.
5
+ * The card is a disclosure over the harness's own settings form: the header
6
+ * folds it (collapsed by default where the page already names the plugin), and
7
+ * the frame inside — read-only notice, controls, save, failure line — is
8
+ * `SettingsForm` with the shared `Switch`, so only the fields this plugin
9
+ * invents are drawn here. Edits are staged until Save, as the form expects.
7
10
  */
8
11
 
9
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
10
- import { Button, Input } from '@deepseek-ai/dsh-client-ui-primitives'
12
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
13
+ import {
14
+ IconChevronDownOutlineRegular, Input, SettingsForm, Switch,
15
+ } from '@deepseek-ai/dsh-client-ui-primitives'
11
16
  import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
12
17
  import type { SettingsPathOpView } from '@deepseek-ai/dsh-api-remotes/client'
13
18
  import type { ConfigForm, ConfigFormSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client'
@@ -77,13 +82,27 @@ function same(a: unknown, b: unknown): boolean {
77
82
  return JSON.stringify(a) === JSON.stringify(b)
78
83
  }
79
84
 
85
+ /** Which seat renders the card: the bundle's own page on the Plugins page, or the Settings fallback tab. */
86
+ export type SettingsCardSeat = 'official' | 'settings-tab'
87
+
80
88
  export type SettingsCardProps = {
81
89
  scope: ConfigForm<ConfigShape>
82
90
  loadModelCatalog: LoadConsolidationModelCatalog
83
91
  loadShelfInventory: LoadShelfInventory
92
+ /**
93
+ * The seat this instance occupies. The Plugins page draws the bundle's title
94
+ * and one-liner above the card, so the card heads itself with the section's
95
+ * own name there; the Settings tab has no such page, so the card carries the
96
+ * plugin's name instead and starts expanded.
97
+ */
98
+ seat: SettingsCardSeat
84
99
  } & PropsLocale<typeof NS>
85
100
 
86
- export function SettingsCard({ scope, loadModelCatalog, loadShelfInventory, t }: SettingsCardProps) {
101
+ export function SettingsCard({ scope, loadModelCatalog, loadShelfInventory, seat, t }: SettingsCardProps) {
102
+ const bodyId = useId()
103
+ // Collapsed by default where the page already names the plugin; the Settings
104
+ // tab is the plugin's whole page, so its card opens.
105
+ const [open, setOpen] = useState(seat === 'settings-tab')
87
106
  const { snap, value } = useScopeValue(scope)
88
107
  const disabled = !snap.writable
89
108
  const base = useMemo(() => (snap.base ?? {}) as ConfigShape, [snap.base])
@@ -360,480 +379,469 @@ export function SettingsCard({ scope, loadModelCatalog, loadShelfInventory, t }:
360
379
  return (
361
380
  <section className={css.card} aria-label={t('title')}>
362
381
  <div className={css.header}>
363
- <span className={css.headText}>
364
- <span className={css.name}>{t('title')}</span>
365
- <span className={css.description}>{t('description')}</span>
366
- </span>
367
- {dirty ? <span className={css.pending}>{t('unsaved')}</span> : null}
382
+ <button
383
+ type="button"
384
+ className={css.toggle}
385
+ aria-expanded={open}
386
+ aria-controls={bodyId}
387
+ onClick={() => { setOpen(value => !value) }}
388
+ >
389
+ <IconChevronDownOutlineRegular className={css.chevron} size={12} aria-hidden="true" />
390
+ {seat === 'settings-tab'
391
+ ? (
392
+ <span className={css.headText}>
393
+ <span className={css.name}>{t('title')}</span>
394
+ <span className={css.description}>{t('description')}</span>
395
+ </span>
396
+ )
397
+ : <span className={css.sectionName}>{t('configSection')}</span>}
398
+ </button>
399
+ {error !== null
400
+ ? <span className={css.failedPill}>{t('saveFailedShort')}</span>
401
+ : dirty ? <span className={css.pending}>{t('unsaved')}</span> : null}
368
402
  </div>
369
403
 
370
- <div className={css.body}>
371
- {!snap.writable ? <p className={css.readOnly} role="status">{t('readOnly')}</p> : null}
372
-
373
- <div className={css.field}>
374
- <div className={css.fieldHead}>
375
- <label className={css.label} htmlFor="ham-enabled">{t('enable')}</label>
376
- {draft.enabled !== (base.enabled ?? true) ? (
377
- <button
378
- type="button"
379
- className={css.reset}
380
- disabled={disabled}
381
- onClick={() => setDraft((current) => ({ ...current, enabled: base.enabled ?? true }))}
382
- >
383
- {t('reset')}
384
- </button>
385
- ) : null}
386
- </div>
387
- <button
388
- id="ham-enabled"
389
- type="button"
390
- role="switch"
391
- aria-checked={draft.enabled ?? true}
392
- aria-label={t('enable')}
393
- className={`${css.switch} ${draft.enabled ?? true ? css.switchOn : ''}`}
394
- disabled={disabled}
395
- onClick={() => setDraft((current) => ({ ...current, enabled: !(current.enabled ?? true) }))}
396
- >
397
- <span className={css.switchThumb} />
398
- </button>
399
- </div>
400
-
401
- <div className={css.field}>
402
- <div className={css.fieldHead}>
403
- <label className={css.label} htmlFor="ham-autoApprove">{t('autoApprove')}</label>
404
- {(draft.autoApprove ?? true) !== (base.autoApprove ?? true) ? (
405
- <button
406
- type="button"
407
- className={css.reset}
408
- disabled={disabled}
409
- onClick={() => setDraft((current) => ({ ...current, autoApprove: base.autoApprove ?? true }))}
410
- >
411
- {t('reset')}
412
- </button>
413
- ) : null}
404
+ {/* Kept mounted while collapsed: a disclosure is not a page change, and a
405
+ staged edit must survive folding the card. */}
406
+ <div className={css.body} id={bodyId} hidden={!open}>
407
+ <SettingsForm
408
+ labels={{
409
+ unavailable: t('unavailable', { ns: NS }),
410
+ readOnly: t('readOnly'),
411
+ saveFailed: t('saveFailed', { message: error ?? '' }),
412
+ save: t('save'),
413
+ saving: t('saving'),
414
+ }}
415
+ // The loading and unavailable documents return above, so the frame
416
+ // this renders is always an available one.
417
+ state={{ available: true, writable: snap.writable, dirty, invalid: false, saving, failed: error !== null }}
418
+ onSave={() => { void save() }}
419
+ onDiscard={discard}
420
+ >
421
+ <div className={css.field}>
422
+ <div className={css.fieldHead}>
423
+ <span className={css.label}>{t('enable')}</span>
424
+ {draft.enabled !== (base.enabled ?? true) ? (
425
+ <button
426
+ type="button"
427
+ className={css.reset}
428
+ disabled={disabled}
429
+ onClick={() => setDraft((current) => ({ ...current, enabled: base.enabled ?? true }))}
430
+ >
431
+ {t('reset')}
432
+ </button>
433
+ ) : null}
434
+ </div>
435
+ <Switch
436
+ checked={draft.enabled ?? true}
437
+ label={t('enable')}
438
+ disabled={disabled}
439
+ onChange={(enabled) => setDraft((current) => ({ ...current, enabled }))}
440
+ />
414
441
  </div>
415
- <button
416
- id="ham-autoApprove"
417
- type="button"
418
- role="switch"
419
- aria-checked={draft.autoApprove ?? true}
420
- aria-label={t('autoApprove')}
421
- className={`${css.switch} ${draft.autoApprove ?? true ? css.switchOn : ''}`}
422
- disabled={disabled}
423
- onClick={() => setDraft((current) => ({ ...current, autoApprove: !(current.autoApprove ?? true) }))}
424
- >
425
- <span className={css.switchThumb} />
426
- </button>
427
- <p className={css.hint}>{t('autoApproveHint')}</p>
428
- </div>
429
-
430
- <div className={css.field}>
431
- <div className={css.fieldHead}>
432
- <label className={css.label} htmlFor="ham-shelf">{t('shelf')}</label>
433
- {draftShelf !== baseShelf ? (
434
- <button
435
- type="button"
436
- className={css.reset}
437
- disabled={disabled}
438
- onClick={() => setDraft((current) => ({ ...current, shelf: baseShelf }))}
439
- >
440
- {t('reset')}
441
- </button>
442
- ) : null}
442
+
443
+ <div className={css.field}>
444
+ <div className={css.fieldHead}>
445
+ <span className={css.label}>{t('autoApprove')}</span>
446
+ {(draft.autoApprove ?? true) !== (base.autoApprove ?? true) ? (
447
+ <button
448
+ type="button"
449
+ className={css.reset}
450
+ disabled={disabled}
451
+ onClick={() => setDraft((current) => ({ ...current, autoApprove: base.autoApprove ?? true }))}
452
+ >
453
+ {t('reset')}
454
+ </button>
455
+ ) : null}
456
+ </div>
457
+ <Switch
458
+ checked={draft.autoApprove ?? true}
459
+ label={t('autoApprove')}
460
+ disabled={disabled}
461
+ onChange={(autoApprove) => setDraft((current) => ({ ...current, autoApprove }))}
462
+ />
463
+ <p className={css.hint}>{t('autoApproveHint')}</p>
443
464
  </div>
444
- <select
445
- id="ham-shelf"
446
- className={css.select}
447
- value={draftShelf}
448
- disabled={disabled || saving}
449
- onChange={(event) => {
450
- const shelf = event.target.value
451
- setDraft((current) => ({ ...current, shelf }))
452
- }}
453
- >
454
- {choices.map(choice => (
455
- <option key={choice.name} value={choice.name}>
456
- {choice.name}
457
- {choice.path !== '' ? ` — ${choice.path}` : ''}
458
- {!choice.listed
459
- ? t('shelfOptionStatus', { status: t('shelfNotListed') })
460
- : !choice.connected ? t('shelfOptionStatus', { status: t('shelfDisconnected') }) : ''}
461
- </option>
462
- ))}
463
- </select>
464
- {shelfStatus === 'loading' ? <p className={css.notice} role="status">{t('shelfLoading')}</p> : null}
465
- {shelfStatus === 'error' ? (
466
- <div className={css.catalogError} role="alert">
467
- <span>{t('shelfLoadFailed')}</span>
468
- <button type="button" disabled={saving} onClick={() => { void loadShelves() }}>{t('retry')}</button>
465
+
466
+ <div className={css.field}>
467
+ <div className={css.fieldHead}>
468
+ <label className={css.label} htmlFor="ham-shelf">{t('shelf')}</label>
469
+ {draftShelf !== baseShelf ? (
470
+ <button
471
+ type="button"
472
+ className={css.reset}
473
+ disabled={disabled}
474
+ onClick={() => setDraft((current) => ({ ...current, shelf: baseShelf }))}
475
+ >
476
+ {t('reset')}
477
+ </button>
478
+ ) : null}
469
479
  </div>
470
- ) : null}
471
- {shelfStatus === 'ready' && shelfListingError !== '' ? (
472
- <p className={css.notice} role="status">{t('shelfListingFailed', { message: shelfListingError })}</p>
473
- ) : null}
474
- {shelfStatus === 'ready' && shelfListingError === '' && chosen !== undefined && (!chosen.listed || !chosen.connected) ? (
475
- <p className={css.warning} role="status">{t(chosen.listed ? 'shelfDisconnectedWarning' : 'shelfNotListedWarning', { shelf: chosen.name })}</p>
476
- ) : null}
477
- <p className={css.hint}>{t('shelfHint')}</p>
478
- </div>
479
-
480
- <section className={css.group} aria-labelledby="ham-consolidation-title">
481
- <h3 id="ham-consolidation-title" className={css.groupTitle}>{t('consolidationTitle')}</h3>
482
- <p className={css.hint}>{t('modelSelectionHint')}</p>
483
-
484
- <div className={css.modelSelection}>
485
- {catalogStatus === 'loading' ? <p className={css.notice} role="status">{t('modelCatalogLoading')}</p> : null}
486
- {catalogStatus === 'error' ? (
480
+ <select
481
+ id="ham-shelf"
482
+ className={css.select}
483
+ value={draftShelf}
484
+ disabled={disabled || saving}
485
+ onChange={(event) => {
486
+ const shelf = event.target.value
487
+ setDraft((current) => ({ ...current, shelf }))
488
+ }}
489
+ >
490
+ {choices.map(choice => (
491
+ <option key={choice.name} value={choice.name}>
492
+ {choice.name}
493
+ {choice.path !== '' ? ` — ${choice.path}` : ''}
494
+ {!choice.listed
495
+ ? t('shelfOptionStatus', { status: t('shelfNotListed') })
496
+ : !choice.connected ? t('shelfOptionStatus', { status: t('shelfDisconnected') }) : ''}
497
+ </option>
498
+ ))}
499
+ </select>
500
+ {shelfStatus === 'loading' ? <p className={css.notice} role="status">{t('shelfLoading')}</p> : null}
501
+ {shelfStatus === 'error' ? (
487
502
  <div className={css.catalogError} role="alert">
488
- <span>{t('modelCatalogFailed')}</span>
489
- <button type="button" disabled={saving} onClick={() => { void loadCatalog() }}>{t('retry')}</button>
503
+ <span>{t('shelfLoadFailed')}</span>
504
+ <button type="button" disabled={saving} onClick={() => { void loadShelves() }}>{t('retry')}</button>
490
505
  </div>
491
506
  ) : null}
492
- {catalogPartial ? <p className={css.notice}>{t('modelCatalogPartial')}</p> : null}
493
- {candidates.length > 0 ? (
494
- <>
495
- <fieldset className={css.models}>
496
- <legend>{t('selectedModels')}</legend>
497
- {selectedModels.length === 0 ? (
498
- <p className={css.notice}>{t('selectedModelsEmpty')}</p>
499
- ) : selectedModels.map((candidate, index) => (
500
- <div key={candidate.key} className={css.selectedRow}>
501
- <span className={css.order}>{index + 1}</span>
502
- <span className={css.selectedText}>
503
- <span className={css.modelName}>{candidate.modelName}</span>
504
- <span className={css.route}>{`${candidate.providerName} · ${candidate.provider}/${candidate.model}`}</span>
505
- </span>
506
- <select
507
- className={css.effort}
508
- aria-label={`${t('reasoningEffort')}: ${candidate.modelName}`}
509
- title={candidate.reasoning === undefined
510
- ? t('reasoningUnavailable')
511
- : routeDefaultLabel(candidate) ?? t('reasoningFollowPurpose')}
512
- value={candidate.reasoningEffort ?? ''}
513
- disabled={disabled || saving || candidate.reasoning === undefined}
514
- onChange={(event) => setModelEffort(index, event.target.value === '' ? undefined : event.target.value)}
515
- >
516
- {reasoningEffortChoices(candidate.reasoning, candidate.reasoningEffort).map(choice => (
517
- <option key={choice.key} value={choice.effort ?? ''}>{effortChoiceLabel(choice)}</option>
518
- ))}
519
- </select>
520
- {!candidate.available ? <span className={css.unavailable}>{t('modelUnavailable')}</span> : null}
521
- <span className={css.rowActions}>
522
- <button
523
- type="button"
524
- className={css.move}
525
- title={t('moveUp')}
526
- aria-label={`${t('moveUp')}: ${candidate.modelName}`}
527
- disabled={disabled || saving || index === 0}
528
- onClick={() => moveModel(index, index - 1)}
529
- >
530
- ↑
531
- </button>
532
- <button
533
- type="button"
534
- className={css.move}
535
- title={t('moveDown')}
536
- aria-label={`${t('moveDown')}: ${candidate.modelName}`}
537
- disabled={disabled || saving || index === selectedModels.length - 1}
538
- onClick={() => moveModel(index, index + 1)}
539
- >
540
- ↓
541
- </button>
542
- <button
543
- type="button"
544
- className={css.move}
545
- title={t('remove')}
546
- aria-label={`${t('remove')}: ${candidate.modelName}`}
547
- disabled={disabled || saving}
548
- onClick={() => toggleModel(candidate)}
549
- >
550
- ×
551
- </button>
552
- </span>
553
- </div>
554
- ))}
555
- </fieldset>
556
-
557
- <fieldset className={css.models}>
558
- <legend>{t('availableModels')}</legend>
559
- {[...availableGroups].map(([provider, group]) => (
560
- <div key={provider} className={css.modelGroup}>
561
- <div className={css.providerName}>{group.providerName}</div>
562
- {group.candidates.map(renderCandidate)}
563
- </div>
564
- ))}
565
- {unavailable.length > 0 ? (
566
- <div className={css.modelGroup}>
567
- <div className={css.providerName}>{t('unavailableModels')}</div>
568
- {unavailable.map(renderCandidate)}
569
- </div>
570
- ) : null}
571
- </fieldset>
572
- </>
573
- ) : catalogStatus === 'ready' ? <p className={css.notice}>{t('modelCatalogEmpty')}</p> : null}
574
- <p className={css.hint}>{t('reasoningBudgetHint')}</p>
507
+ {shelfStatus === 'ready' && shelfListingError !== '' ? (
508
+ <p className={css.notice} role="status">{t('shelfListingFailed', { message: shelfListingError })}</p>
509
+ ) : null}
510
+ {shelfStatus === 'ready' && shelfListingError === '' && chosen !== undefined && (!chosen.listed || !chosen.connected) ? (
511
+ <p className={css.warning} role="status">{t(chosen.listed ? 'shelfDisconnectedWarning' : 'shelfNotListedWarning', { shelf: chosen.name })}</p>
512
+ ) : null}
513
+ <p className={css.hint}>{t('shelfHint')}</p>
575
514
  </div>
576
515
 
577
- <div className={css.pairedFields}>
578
- <div className={css.field}>
579
- <div className={css.fieldHead}>
580
- <label className={css.label} htmlFor="ham-checkEveryTurns">{t('checkEveryTurns')}</label>
581
- {consolidation.checkEveryTurns !== (base.consolidation?.checkEveryTurns ?? DEFAULT_CONSOLIDATION.checkEveryTurns) ? (
582
- <button
583
- type="button"
584
- className={css.reset}
585
- disabled={disabled}
586
- onClick={() => updateConsolidation({ checkEveryTurns: base.consolidation?.checkEveryTurns ?? DEFAULT_CONSOLIDATION.checkEveryTurns })}
587
- >
588
- {t('reset')}
589
- </button>
590
- ) : null}
591
- </div>
592
- <Input
593
- id="ham-checkEveryTurns"
594
- className={css.input}
595
- type="number"
596
- min={1}
597
- value={consolidation.checkEveryTurns}
598
- disabled={disabled}
599
- onChange={(event) => updateConsolidation({ checkEveryTurns: Number(event.target.value) })}
600
- />
601
- </div>
602
- <div className={css.field}>
603
- <div className={css.fieldHead}>
604
- <label className={css.label} htmlFor="ham-minNewTokens">{t('minNewTokens')}</label>
605
- {consolidation.minNewTokens !== (base.consolidation?.minNewTokens ?? DEFAULT_CONSOLIDATION.minNewTokens) ? (
606
- <button
607
- type="button"
608
- className={css.reset}
609
- disabled={disabled}
610
- onClick={() => updateConsolidation({ minNewTokens: base.consolidation?.minNewTokens ?? DEFAULT_CONSOLIDATION.minNewTokens })}
611
- >
612
- {t('reset')}
613
- </button>
614
- ) : null}
615
- </div>
616
- <Input
617
- id="ham-minNewTokens"
618
- className={css.input}
619
- type="number"
620
- min={0}
621
- value={consolidation.minNewTokens}
622
- disabled={disabled}
623
- onChange={(event) => updateConsolidation({ minNewTokens: Number(event.target.value) })}
624
- />
516
+ <section className={css.group} aria-labelledby="ham-consolidation-title">
517
+ <h3 id="ham-consolidation-title" className={css.groupTitle}>{t('consolidationTitle')}</h3>
518
+ <p className={css.hint}>{t('modelSelectionHint')}</p>
519
+
520
+ <div className={css.modelSelection}>
521
+ {catalogStatus === 'loading' ? <p className={css.notice} role="status">{t('modelCatalogLoading')}</p> : null}
522
+ {catalogStatus === 'error' ? (
523
+ <div className={css.catalogError} role="alert">
524
+ <span>{t('modelCatalogFailed')}</span>
525
+ <button type="button" disabled={saving} onClick={() => { void loadCatalog() }}>{t('retry')}</button>
526
+ </div>
527
+ ) : null}
528
+ {catalogPartial ? <p className={css.notice}>{t('modelCatalogPartial')}</p> : null}
529
+ {candidates.length > 0 ? (
530
+ <>
531
+ <fieldset className={css.models}>
532
+ <legend>{t('selectedModels')}</legend>
533
+ {selectedModels.length === 0 ? (
534
+ <p className={css.notice}>{t('selectedModelsEmpty')}</p>
535
+ ) : selectedModels.map((candidate, index) => (
536
+ <div key={candidate.key} className={css.selectedRow}>
537
+ <span className={css.order}>{index + 1}</span>
538
+ <span className={css.selectedText}>
539
+ <span className={css.modelName}>{candidate.modelName}</span>
540
+ <span className={css.route}>{`${candidate.providerName} · ${candidate.provider}/${candidate.model}`}</span>
541
+ </span>
542
+ <select
543
+ className={css.effort}
544
+ aria-label={`${t('reasoningEffort')}: ${candidate.modelName}`}
545
+ title={candidate.reasoning === undefined
546
+ ? t('reasoningUnavailable')
547
+ : routeDefaultLabel(candidate) ?? t('reasoningFollowPurpose')}
548
+ value={candidate.reasoningEffort ?? ''}
549
+ disabled={disabled || saving || candidate.reasoning === undefined}
550
+ onChange={(event) => setModelEffort(index, event.target.value === '' ? undefined : event.target.value)}
551
+ >
552
+ {reasoningEffortChoices(candidate.reasoning, candidate.reasoningEffort).map(choice => (
553
+ <option key={choice.key} value={choice.effort ?? ''}>{effortChoiceLabel(choice)}</option>
554
+ ))}
555
+ </select>
556
+ {!candidate.available ? <span className={css.unavailable}>{t('modelUnavailable')}</span> : null}
557
+ <span className={css.rowActions}>
558
+ <button
559
+ type="button"
560
+ className={css.move}
561
+ title={t('moveUp')}
562
+ aria-label={`${t('moveUp')}: ${candidate.modelName}`}
563
+ disabled={disabled || saving || index === 0}
564
+ onClick={() => moveModel(index, index - 1)}
565
+ >
566
+ ↑
567
+ </button>
568
+ <button
569
+ type="button"
570
+ className={css.move}
571
+ title={t('moveDown')}
572
+ aria-label={`${t('moveDown')}: ${candidate.modelName}`}
573
+ disabled={disabled || saving || index === selectedModels.length - 1}
574
+ onClick={() => moveModel(index, index + 1)}
575
+ >
576
+ ↓
577
+ </button>
578
+ <button
579
+ type="button"
580
+ className={css.move}
581
+ title={t('remove')}
582
+ aria-label={`${t('remove')}: ${candidate.modelName}`}
583
+ disabled={disabled || saving}
584
+ onClick={() => toggleModel(candidate)}
585
+ >
586
+ ×
587
+ </button>
588
+ </span>
589
+ </div>
590
+ ))}
591
+ </fieldset>
592
+
593
+ <fieldset className={css.models}>
594
+ <legend>{t('availableModels')}</legend>
595
+ {[...availableGroups].map(([provider, group]) => (
596
+ <div key={provider} className={css.modelGroup}>
597
+ <div className={css.providerName}>{group.providerName}</div>
598
+ {group.candidates.map(renderCandidate)}
599
+ </div>
600
+ ))}
601
+ {unavailable.length > 0 ? (
602
+ <div className={css.modelGroup}>
603
+ <div className={css.providerName}>{t('unavailableModels')}</div>
604
+ {unavailable.map(renderCandidate)}
605
+ </div>
606
+ ) : null}
607
+ </fieldset>
608
+ </>
609
+ ) : catalogStatus === 'ready' ? <p className={css.notice}>{t('modelCatalogEmpty')}</p> : null}
610
+ <p className={css.hint}>{t('reasoningBudgetHint')}</p>
625
611
  </div>
626
- </div>
627
612
 
628
- <div className={css.pairedFields}>
629
- <div className={css.field}>
630
- <div className={css.fieldHead}>
631
- <label className={css.label} htmlFor="ham-cascadeBatchSize">{t('cascadeBatchSize')}</label>
632
- {(consolidation.cascade?.batchSize ?? DEFAULT_CONSOLIDATION.cascade.batchSize)
633
- !== (base.consolidation?.cascade?.batchSize ?? DEFAULT_CONSOLIDATION.cascade.batchSize) ? (
613
+ <div className={css.pairedFields}>
614
+ <div className={css.field}>
615
+ <div className={css.fieldHead}>
616
+ <label className={css.label} htmlFor="ham-checkEveryTurns">{t('checkEveryTurns')}</label>
617
+ {consolidation.checkEveryTurns !== (base.consolidation?.checkEveryTurns ?? DEFAULT_CONSOLIDATION.checkEveryTurns) ? (
634
618
  <button
635
619
  type="button"
636
620
  className={css.reset}
637
621
  disabled={disabled}
638
- onClick={() => updateConsolidation({
639
- cascade: {
640
- ...(consolidation.cascade ?? DEFAULT_CONSOLIDATION.cascade),
641
- batchSize: base.consolidation?.cascade?.batchSize ?? DEFAULT_CONSOLIDATION.cascade.batchSize,
642
- },
643
- })}
622
+ onClick={() => updateConsolidation({ checkEveryTurns: base.consolidation?.checkEveryTurns ?? DEFAULT_CONSOLIDATION.checkEveryTurns })}
644
623
  >
645
624
  {t('reset')}
646
625
  </button>
647
626
  ) : null}
627
+ </div>
628
+ <Input
629
+ id="ham-checkEveryTurns"
630
+ className={css.input}
631
+ type="number"
632
+ min={1}
633
+ value={consolidation.checkEveryTurns}
634
+ disabled={disabled}
635
+ onChange={(event) => updateConsolidation({ checkEveryTurns: Number(event.target.value) })}
636
+ />
648
637
  </div>
649
- <Input
650
- id="ham-cascadeBatchSize"
651
- className={css.input}
652
- type="number"
653
- min={2}
654
- value={consolidation.cascade?.batchSize ?? DEFAULT_CONSOLIDATION.cascade.batchSize}
655
- disabled={disabled}
656
- onChange={(event) => updateConsolidation({
657
- cascade: {
658
- ...(consolidation.cascade ?? DEFAULT_CONSOLIDATION.cascade),
659
- batchSize: Number(event.target.value),
660
- },
661
- })}
662
- />
663
- </div>
664
- <div className={css.field}>
665
- <div className={css.fieldHead}>
666
- <label className={css.label} htmlFor="ham-dedupMaxDistance">{t('dedupMaxDistance')}</label>
667
- {(consolidation.dedupMaxDistance ?? DEFAULT_CONSOLIDATION.dedupMaxDistance)
668
- !== (base.consolidation?.dedupMaxDistance ?? DEFAULT_CONSOLIDATION.dedupMaxDistance) ? (
638
+ <div className={css.field}>
639
+ <div className={css.fieldHead}>
640
+ <label className={css.label} htmlFor="ham-minNewTokens">{t('minNewTokens')}</label>
641
+ {consolidation.minNewTokens !== (base.consolidation?.minNewTokens ?? DEFAULT_CONSOLIDATION.minNewTokens) ? (
669
642
  <button
670
643
  type="button"
671
644
  className={css.reset}
672
645
  disabled={disabled}
673
- onClick={() => updateConsolidation({
674
- dedupMaxDistance: base.consolidation?.dedupMaxDistance ?? DEFAULT_CONSOLIDATION.dedupMaxDistance,
675
- })}
646
+ onClick={() => updateConsolidation({ minNewTokens: base.consolidation?.minNewTokens ?? DEFAULT_CONSOLIDATION.minNewTokens })}
676
647
  >
677
648
  {t('reset')}
678
649
  </button>
679
650
  ) : null}
651
+ </div>
652
+ <Input
653
+ id="ham-minNewTokens"
654
+ className={css.input}
655
+ type="number"
656
+ min={0}
657
+ value={consolidation.minNewTokens}
658
+ disabled={disabled}
659
+ onChange={(event) => updateConsolidation({ minNewTokens: Number(event.target.value) })}
660
+ />
680
661
  </div>
681
- <Input
682
- id="ham-dedupMaxDistance"
683
- className={css.input}
684
- type="number"
685
- min={0}
686
- step={0.05}
687
- value={consolidation.dedupMaxDistance ?? DEFAULT_CONSOLIDATION.dedupMaxDistance}
688
- disabled={disabled}
689
- onChange={(event) => updateConsolidation({ dedupMaxDistance: Number(event.target.value) })}
690
- />
691
662
  </div>
692
- </div>
693
- <p className={css.hint}>{t('cascadeHint')}</p>
694
-
695
- <h4 className={css.limitsTitle}>{t('limitsTitle')}</h4>
696
- <p className={css.hint}>{t('limitsHint')}</p>
697
- <div className={css.pairedFields}>
698
- <div className={css.field}>
699
- <div className={css.fieldHead}>
700
- <label className={css.label} htmlFor="ham-maxInputTokens">{t('maxInputTokens')}</label>
701
- {consolidation.maxInputTokens !== (base.consolidation?.maxInputTokens ?? DEFAULT_CONSOLIDATION.maxInputTokens) ? (
702
- <button
703
- type="button"
704
- className={css.reset}
705
- disabled={disabled}
706
- onClick={() => updateConsolidation({ maxInputTokens: base.consolidation?.maxInputTokens ?? DEFAULT_CONSOLIDATION.maxInputTokens })}
707
- >
708
- {t('reset')}
709
- </button>
710
- ) : null}
663
+
664
+ <div className={css.pairedFields}>
665
+ <div className={css.field}>
666
+ <div className={css.fieldHead}>
667
+ <label className={css.label} htmlFor="ham-cascadeBatchSize">{t('cascadeBatchSize')}</label>
668
+ {(consolidation.cascade?.batchSize ?? DEFAULT_CONSOLIDATION.cascade.batchSize)
669
+ !== (base.consolidation?.cascade?.batchSize ?? DEFAULT_CONSOLIDATION.cascade.batchSize) ? (
670
+ <button
671
+ type="button"
672
+ className={css.reset}
673
+ disabled={disabled}
674
+ onClick={() => updateConsolidation({
675
+ cascade: {
676
+ ...(consolidation.cascade ?? DEFAULT_CONSOLIDATION.cascade),
677
+ batchSize: base.consolidation?.cascade?.batchSize ?? DEFAULT_CONSOLIDATION.cascade.batchSize,
678
+ },
679
+ })}
680
+ >
681
+ {t('reset')}
682
+ </button>
683
+ ) : null}
684
+ </div>
685
+ <Input
686
+ id="ham-cascadeBatchSize"
687
+ className={css.input}
688
+ type="number"
689
+ min={2}
690
+ value={consolidation.cascade?.batchSize ?? DEFAULT_CONSOLIDATION.cascade.batchSize}
691
+ disabled={disabled}
692
+ onChange={(event) => updateConsolidation({
693
+ cascade: {
694
+ ...(consolidation.cascade ?? DEFAULT_CONSOLIDATION.cascade),
695
+ batchSize: Number(event.target.value),
696
+ },
697
+ })}
698
+ />
711
699
  </div>
712
- <Input
713
- id="ham-maxInputTokens"
714
- className={css.input}
715
- type="number"
716
- min={1000}
717
- step={1000}
718
- value={consolidation.maxInputTokens}
719
- disabled={disabled}
720
- onChange={(event) => updateConsolidation({ maxInputTokens: Number(event.target.value) })}
721
- />
722
- </div>
723
- <div className={css.field}>
724
- <div className={css.fieldHead}>
725
- <label className={css.label} htmlFor="ham-maxOutputTokens">{t('maxOutputTokens')}</label>
726
- {consolidation.maxOutputTokens !== (base.consolidation?.maxOutputTokens ?? DEFAULT_CONSOLIDATION.maxOutputTokens) ? (
727
- <button
728
- type="button"
729
- className={css.reset}
730
- disabled={disabled}
731
- onClick={() => updateConsolidation({ maxOutputTokens: base.consolidation?.maxOutputTokens ?? DEFAULT_CONSOLIDATION.maxOutputTokens })}
732
- >
733
- {t('reset')}
734
- </button>
735
- ) : null}
700
+ <div className={css.field}>
701
+ <div className={css.fieldHead}>
702
+ <label className={css.label} htmlFor="ham-dedupMaxDistance">{t('dedupMaxDistance')}</label>
703
+ {(consolidation.dedupMaxDistance ?? DEFAULT_CONSOLIDATION.dedupMaxDistance)
704
+ !== (base.consolidation?.dedupMaxDistance ?? DEFAULT_CONSOLIDATION.dedupMaxDistance) ? (
705
+ <button
706
+ type="button"
707
+ className={css.reset}
708
+ disabled={disabled}
709
+ onClick={() => updateConsolidation({
710
+ dedupMaxDistance: base.consolidation?.dedupMaxDistance ?? DEFAULT_CONSOLIDATION.dedupMaxDistance,
711
+ })}
712
+ >
713
+ {t('reset')}
714
+ </button>
715
+ ) : null}
716
+ </div>
717
+ <Input
718
+ id="ham-dedupMaxDistance"
719
+ className={css.input}
720
+ type="number"
721
+ min={0}
722
+ step={0.05}
723
+ value={consolidation.dedupMaxDistance ?? DEFAULT_CONSOLIDATION.dedupMaxDistance}
724
+ disabled={disabled}
725
+ onChange={(event) => updateConsolidation({ dedupMaxDistance: Number(event.target.value) })}
726
+ />
736
727
  </div>
737
- <Input
738
- id="ham-maxOutputTokens"
739
- className={css.input}
740
- type="number"
741
- min={200}
742
- step={200}
743
- value={consolidation.maxOutputTokens}
744
- disabled={disabled}
745
- onChange={(event) => updateConsolidation({ maxOutputTokens: Number(event.target.value) })}
746
- />
747
728
  </div>
748
- <div className={css.field}>
749
- <div className={css.fieldHead}>
750
- <label className={css.label} htmlFor="ham-maxWorkUnitsPerRun">{t('maxWorkUnitsPerRun')}</label>
751
- {consolidation.maxWorkUnitsPerRun !== (base.consolidation?.maxWorkUnitsPerRun ?? DEFAULT_CONSOLIDATION.maxWorkUnitsPerRun) ? (
752
- <button
753
- type="button"
754
- className={css.reset}
755
- disabled={disabled}
756
- onClick={() => updateConsolidation({ maxWorkUnitsPerRun: base.consolidation?.maxWorkUnitsPerRun ?? DEFAULT_CONSOLIDATION.maxWorkUnitsPerRun })}
757
- >
758
- {t('reset')}
759
- </button>
760
- ) : null}
729
+ <p className={css.hint}>{t('cascadeHint')}</p>
730
+
731
+ <h4 className={css.limitsTitle}>{t('limitsTitle')}</h4>
732
+ <p className={css.hint}>{t('limitsHint')}</p>
733
+ <div className={css.pairedFields}>
734
+ <div className={css.field}>
735
+ <div className={css.fieldHead}>
736
+ <label className={css.label} htmlFor="ham-maxInputTokens">{t('maxInputTokens')}</label>
737
+ {consolidation.maxInputTokens !== (base.consolidation?.maxInputTokens ?? DEFAULT_CONSOLIDATION.maxInputTokens) ? (
738
+ <button
739
+ type="button"
740
+ className={css.reset}
741
+ disabled={disabled}
742
+ onClick={() => updateConsolidation({ maxInputTokens: base.consolidation?.maxInputTokens ?? DEFAULT_CONSOLIDATION.maxInputTokens })}
743
+ >
744
+ {t('reset')}
745
+ </button>
746
+ ) : null}
747
+ </div>
748
+ <Input
749
+ id="ham-maxInputTokens"
750
+ className={css.input}
751
+ type="number"
752
+ min={1000}
753
+ step={1000}
754
+ value={consolidation.maxInputTokens}
755
+ disabled={disabled}
756
+ onChange={(event) => updateConsolidation({ maxInputTokens: Number(event.target.value) })}
757
+ />
758
+ </div>
759
+ <div className={css.field}>
760
+ <div className={css.fieldHead}>
761
+ <label className={css.label} htmlFor="ham-maxOutputTokens">{t('maxOutputTokens')}</label>
762
+ {consolidation.maxOutputTokens !== (base.consolidation?.maxOutputTokens ?? DEFAULT_CONSOLIDATION.maxOutputTokens) ? (
763
+ <button
764
+ type="button"
765
+ className={css.reset}
766
+ disabled={disabled}
767
+ onClick={() => updateConsolidation({ maxOutputTokens: base.consolidation?.maxOutputTokens ?? DEFAULT_CONSOLIDATION.maxOutputTokens })}
768
+ >
769
+ {t('reset')}
770
+ </button>
771
+ ) : null}
772
+ </div>
773
+ <Input
774
+ id="ham-maxOutputTokens"
775
+ className={css.input}
776
+ type="number"
777
+ min={200}
778
+ step={200}
779
+ value={consolidation.maxOutputTokens}
780
+ disabled={disabled}
781
+ onChange={(event) => updateConsolidation({ maxOutputTokens: Number(event.target.value) })}
782
+ />
783
+ </div>
784
+ <div className={css.field}>
785
+ <div className={css.fieldHead}>
786
+ <label className={css.label} htmlFor="ham-maxWorkUnitsPerRun">{t('maxWorkUnitsPerRun')}</label>
787
+ {consolidation.maxWorkUnitsPerRun !== (base.consolidation?.maxWorkUnitsPerRun ?? DEFAULT_CONSOLIDATION.maxWorkUnitsPerRun) ? (
788
+ <button
789
+ type="button"
790
+ className={css.reset}
791
+ disabled={disabled}
792
+ onClick={() => updateConsolidation({ maxWorkUnitsPerRun: base.consolidation?.maxWorkUnitsPerRun ?? DEFAULT_CONSOLIDATION.maxWorkUnitsPerRun })}
793
+ >
794
+ {t('reset')}
795
+ </button>
796
+ ) : null}
797
+ </div>
798
+ <Input
799
+ id="ham-maxWorkUnitsPerRun"
800
+ className={css.input}
801
+ type="number"
802
+ min={1}
803
+ max={10}
804
+ value={consolidation.maxWorkUnitsPerRun}
805
+ disabled={disabled}
806
+ onChange={(event) => updateConsolidation({ maxWorkUnitsPerRun: Number(event.target.value) })}
807
+ />
761
808
  </div>
762
- <Input
763
- id="ham-maxWorkUnitsPerRun"
764
- className={css.input}
765
- type="number"
766
- min={1}
767
- max={10}
768
- value={consolidation.maxWorkUnitsPerRun}
769
- disabled={disabled}
770
- onChange={(event) => updateConsolidation({ maxWorkUnitsPerRun: Number(event.target.value) })}
771
- />
772
809
  </div>
810
+ </section>
811
+
812
+ <div className={css.field}>
813
+ <div className={css.fieldHead}>
814
+ <span className={css.label}>{t('recallPreload')}</span>
815
+ {(recall.preloadRulesTaboos ?? true) !== (base.recall?.preloadRulesTaboos ?? true) ? (
816
+ <button
817
+ type="button"
818
+ className={css.reset}
819
+ disabled={disabled}
820
+ onClick={() => setDraft((current) => ({
821
+ ...current,
822
+ recall: {
823
+ ...(current.recall ?? { preloadRulesTaboos: true }),
824
+ preloadRulesTaboos: base.recall?.preloadRulesTaboos ?? true,
825
+ },
826
+ }))}
827
+ >
828
+ {t('reset')}
829
+ </button>
830
+ ) : null}
831
+ </div>
832
+ <Switch
833
+ checked={recall.preloadRulesTaboos ?? true}
834
+ label={t('recallPreload')}
835
+ disabled={disabled}
836
+ onChange={(preloadRulesTaboos) => setDraft((current) => ({
837
+ ...current,
838
+ recall: { ...(current.recall ?? { preloadRulesTaboos: true }), preloadRulesTaboos },
839
+ }))}
840
+ />
773
841
  </div>
774
- </section>
775
-
776
- <div className={css.field}>
777
- <div className={css.fieldHead}>
778
- <label className={css.label} htmlFor="ham-preloadRulesTaboos">{t('recallPreload')}</label>
779
- {(recall.preloadRulesTaboos ?? true) !== (base.recall?.preloadRulesTaboos ?? true) ? (
780
- <button
781
- type="button"
782
- className={css.reset}
783
- disabled={disabled}
784
- onClick={() => setDraft((current) => ({
785
- ...current,
786
- recall: {
787
- ...(current.recall ?? { preloadRulesTaboos: true }),
788
- preloadRulesTaboos: base.recall?.preloadRulesTaboos ?? true,
789
- },
790
- }))}
791
- >
792
- {t('reset')}
793
- </button>
794
- ) : null}
795
- </div>
796
- <button
797
- id="ham-preloadRulesTaboos"
798
- type="button"
799
- role="switch"
800
- aria-checked={recall.preloadRulesTaboos ?? true}
801
- aria-label={t('recallPreload')}
802
- className={`${css.switch} ${recall.preloadRulesTaboos ?? true ? css.switchOn : ''}`}
803
- disabled={disabled}
804
- onClick={() => setDraft((current) => ({
805
- ...current,
806
- recall: {
807
- ...(current.recall ?? { preloadRulesTaboos: true }),
808
- preloadRulesTaboos: !(current.recall?.preloadRulesTaboos ?? true),
809
- },
810
- }))}
811
- >
812
- <span className={css.switchThumb} />
813
- </button>
814
- </div>
815
-
816
- <p className={css.advancedHint}>{t('advancedHint', { ns: NS })}</p>
817
- {error ? <p className={css.error} role="status">{t('saveFailed', { message: error })}</p> : null}
818
-
819
- <div className={css.footer}>
820
- <Button
821
- variant="outline"
822
- size="sm"
823
- disabled={!dirty || saving}
824
- onClick={discard}
825
- >
826
- {t('discard')}
827
- </Button>
828
- <Button
829
- variant="primary"
830
- size="sm"
831
- disabled={saveBlocked}
832
- onClick={save}
833
- >
834
- {saving ? t('saving') : t('save')}
835
- </Button>
836
- </div>
842
+
843
+ <p className={css.advancedHint}>{t('advancedHint', { ns: NS })}</p>
844
+ </SettingsForm>
837
845
  </div>
838
846
  </section>
839
847
  )