@wowyuarm/dsh-agent-team 0.2.0 → 0.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.
Files changed (67) hide show
  1. package/README.md +5 -5
  2. package/README.zh.md +5 -5
  3. package/cordis.patch.yml +26 -12
  4. package/package.json +50 -47
  5. package/packages/agent-team/lib/attachments.js +63 -4
  6. package/packages/agent-team/lib/compaction-engine.js +18 -0
  7. package/packages/agent-team/lib/context-continuity-host.js +198 -4
  8. package/packages/agent-team/lib/context-judge.js +150 -0
  9. package/packages/agent-team/lib/index.js +482 -97
  10. package/packages/agent-team/lib/invariant.js +36 -12
  11. package/packages/agent-team/lib/ledger.js +2 -1
  12. package/packages/agent-team/lib/member-runtime.js +2 -1
  13. package/packages/agent-team/lib/recovery.js +53 -4
  14. package/packages/agent-team/lib/typert.host.js +138 -44
  15. package/packages/agent-team/lib/typert.remote-client.d.ts +3 -1
  16. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  17. package/packages/agent-team/lib/typert.remote-client.js +75 -35
  18. package/packages/agent-team/lib/types/attachments.d.ts +32 -2
  19. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
  20. package/packages/agent-team/lib/types/compaction-engine.d.ts +19 -0
  21. package/packages/agent-team/lib/types/compaction-engine.d.ts.map +1 -0
  22. package/packages/agent-team/lib/types/context-continuity-host.d.ts +131 -4
  23. package/packages/agent-team/lib/types/context-continuity-host.d.ts.map +1 -1
  24. package/packages/agent-team/lib/types/context-judge.d.ts +82 -0
  25. package/packages/agent-team/lib/types/context-judge.d.ts.map +1 -0
  26. package/packages/agent-team/lib/types/index.d.ts +259 -17
  27. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  28. package/packages/agent-team/lib/types/invariant.d.ts +27 -5
  29. package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
  30. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  31. package/packages/agent-team/lib/types/member-runtime.d.ts +1 -1
  32. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
  33. package/packages/agent-team/lib/types/recovery.d.ts +19 -6
  34. package/packages/agent-team/lib/types/recovery.d.ts.map +1 -1
  35. package/packages/agent-team/lib/types/types/requests-results.d.ts +28 -2
  36. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  37. package/packages/agent-team/lib/types/types.d.ts +9 -0
  38. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  39. package/packages/client-agent-team/lib/client.js +1006 -374
  40. package/packages/client-agent-team/lib/client.js.map +1 -1
  41. package/packages/client-agent-team/lib/types/client/{HumanSettingsSection.d.ts → TeamSettingsSection.d.ts} +12 -4
  42. package/packages/client-agent-team/lib/types/client/TeamSettingsSection.d.ts.map +1 -0
  43. package/packages/client-agent-team/lib/types/client/TeamSettingsSection.js +195 -0
  44. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +7 -11
  45. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -1
  46. package/packages/client-agent-team/lib/types/client/attachment-preview.js +13 -3
  47. package/packages/client-agent-team/lib/types/client/context-judge.d.ts +64 -0
  48. package/packages/client-agent-team/lib/types/client/context-judge.d.ts.map +1 -0
  49. package/packages/client-agent-team/lib/types/client/context-judge.js +78 -0
  50. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  51. package/packages/client-agent-team/lib/types/client/index.js +75 -11
  52. package/packages/client-agent-team/lib/types/client/judge-form.d.ts +99 -0
  53. package/packages/client-agent-team/lib/types/client/judge-form.d.ts.map +1 -0
  54. package/packages/client-agent-team/lib/types/client/judge-form.js +208 -0
  55. package/packages/client-agent-team/lib/types/client/locales.d.ts +52 -6
  56. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  57. package/packages/client-agent-team/lib/types/client/locales.js +55 -6
  58. package/packages/tool-agent-team/README.md +6 -3
  59. package/packages/tool-agent-team/README.zh.md +6 -3
  60. package/packages/tool-agent-team/lib/context-tools.js +84 -112
  61. package/packages/tool-agent-team/lib/types/context-tools.d.ts +29 -13
  62. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
  63. package/packages/agent-team/lib/pressure-policy.js +0 -235
  64. package/packages/agent-team/lib/types/pressure-policy.d.ts +0 -134
  65. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +0 -1
  66. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts.map +0 -1
  67. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.js +0 -118
@@ -1,7 +1,9 @@
1
1
  import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
2
2
  import { type TeamHumanIdentityFace } from './human-identity.ts';
3
3
  import type { TeamEnvironmentSource } from './environment-check.ts';
4
- export interface HumanSettingsSectionInjected {
4
+ import { type TeamContextJudgeSource } from './context-judge.ts';
5
+ import type { TeamJudgeFormSource } from './judge-form.ts';
6
+ export interface TeamSettingsInjected {
5
7
  /** The shared Human identity projection: profile facts plus post-write refresh. */
6
8
  identity: TeamHumanIdentityFace;
7
9
  /** Persist one renamed display name; the failure message when the Host refuses it. */
@@ -12,7 +14,13 @@ export interface HumanSettingsSectionInjected {
12
14
  removeAvatar: () => Promise<string | undefined>;
13
15
  /** The local environment check, a read-only projection of the installation. */
14
16
  environment: TeamEnvironmentSource;
17
+ /** The judge's live status: whether one is reachable right now. */
18
+ judge: TeamContextJudgeSource;
19
+ /** The staged form over the judge's endpoint; it holds nothing while this deployment serves no settings document. */
20
+ judgeForm: TeamJudgeFormSource;
21
+ /** Whether the settings document holds a key literal for the judge. */
22
+ keyConfigured: () => boolean;
15
23
  }
16
- export type HumanSettingsSectionProps = PropsRuntime<'settings.section'> & PropsLocale<'team'> & HumanSettingsSectionInjected;
17
- export declare function HumanSettingsSection(props: HumanSettingsSectionProps): import("react").JSX.Element;
18
- //# sourceMappingURL=HumanSettingsSection.d.ts.map
24
+ export type TeamSettingsProps = PropsRuntime<'settings.section'> & PropsLocale<'team'> & TeamSettingsInjected;
25
+ export declare function TeamSettingsSection(props: TeamSettingsProps): import("react").JSX.Element;
26
+ //# sourceMappingURL=TeamSettingsSection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TeamSettingsSection.d.ts","sourceRoot":"","sources":["../../../src/client/TeamSettingsSection.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAMjF,OAAO,EAAoB,KAAK,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAElF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAEnE,OAAO,EAAmB,KAAK,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AACjF,OAAO,KAAK,EAAiC,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAoBzF,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,sFAAsF;IACtF,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IACvD,gGAAgG;IAChG,YAAY,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IACzD,gEAAgE;IAChE,YAAY,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAC/C,+EAA+E;IAC/E,WAAW,EAAE,qBAAqB,CAAA;IAClC,mEAAmE;IACnE,KAAK,EAAE,sBAAsB,CAAA;IAC7B,qHAAqH;IACrH,SAAS,EAAE,mBAAmB,CAAA;IAC9B,uEAAuE;IACvE,aAAa,EAAE,MAAM,OAAO,CAAA;CAC7B;AAED,MAAM,MAAM,iBAAiB,GACzB,YAAY,CAAC,kBAAkB,CAAC,GAChC,WAAW,CAAC,MAAM,CAAC,GACnB,oBAAoB,CAAA;AAsBxB,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,iBAAiB,+BA+K3D"}
@@ -0,0 +1,195 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState, useSyncExternalStore } from 'react';
3
+ import { Button, IconChevronDownOutlineRegular, IconChevronRightOutlineRegular, Input, SettingsForm, SettingsSecretField, SettingsValueField, } from '@deepseek-ai/dsh-client-ui-primitives';
4
+ import { useHumanIdentity } from "./human-identity.js";
5
+ import { EnvironmentCheck } from "./EnvironmentCheck.js";
6
+ import { useAvatarImage } from "./avatar-image.js";
7
+ import { useContextJudge } from "./context-judge.js";
8
+ import css from './team-settings.module.css';
9
+ /**
10
+ * The Team's settings page: the Human's display name and avatar, the endpoint
11
+ * the relatedness judge calls, the local environment check, and the version
12
+ * footnote.
13
+ *
14
+ * The page owns no durable fact and no copy of one, and it holds its two
15
+ * writable groups apart because they are two different documents: identity
16
+ * arrives from the shared identity projection and writes through the Team
17
+ * Remote, while the judge's endpoint is one group of the Team row's settings
18
+ * section and writes through the settings provider. Neither group renders the
19
+ * other's fields, and a failed write in either keeps the typed value and
20
+ * reports the reason it was refused.
21
+ */
22
+ /** Host-side avatar ceiling (`ATTACHMENT_MAX_BYTES`): the settings page refuses larger files before the round trip. */
23
+ const AVATAR_MAX_BYTES = 10 * 1024 * 1024;
24
+ /**
25
+ * Run one durable write and reduce every failure to the message the page
26
+ * reports. A rejected Remote call (a dropped carrier, a refused write) is a
27
+ * failure like any other: without this the field would sit on "saving" forever
28
+ * and report nothing.
29
+ */
30
+ async function failureOf(action) {
31
+ try {
32
+ return await action();
33
+ }
34
+ catch (error) {
35
+ return error instanceof Error ? error.message : String(error);
36
+ }
37
+ }
38
+ /** First visible character of a display name, or the neutral `H` before one is known. */
39
+ function avatarInitial(name) {
40
+ const trimmed = (name ?? '').replace(/^@/, '').trim();
41
+ return trimmed === '' ? 'H' : trimmed.slice(0, 1).toUpperCase();
42
+ }
43
+ export function TeamSettingsSection(props) {
44
+ const { t, identity } = props;
45
+ const profile = useHumanIdentity(identity);
46
+ // The endpoint form is observed rather than received: the settings service it
47
+ // is built over can arrive after this entry's injected props were computed,
48
+ // and those props are cached for the entry's lifetime, so a value handed over
49
+ // once would never appear.
50
+ const judgeForm = useSyncExternalStore(props.judgeForm.subscribe, props.judgeForm.getSnapshot);
51
+ // Undefined means "follow the Host value": the field re-syncs whenever the
52
+ // profile changes underneath, without a background read clobbering a name
53
+ // the reader is still editing.
54
+ const [draft, setDraft] = useState(undefined);
55
+ const [saving, setSaving] = useState(false);
56
+ const [uploading, setUploading] = useState(false);
57
+ const [notice, setNotice] = useState(null);
58
+ const filePicker = useRef(null);
59
+ const name = draft ?? profile.name ?? '';
60
+ const dirty = draft !== undefined && draft.trim() !== (profile.name ?? '');
61
+ const emptyName = draft !== undefined && draft.trim() === '';
62
+ const hasAvatar = profile.avatarRef !== undefined;
63
+ // The circle answers to decoded bytes, not to a stored reference: the Host
64
+ // accepts any `image/…` payload, so a file this browser cannot read has to
65
+ // fall back to the initial exactly as a removed one does.
66
+ const avatar = useAvatarImage(profile.avatarUrl);
67
+ const submitName = async () => {
68
+ if (!dirty || emptyName || saving)
69
+ return;
70
+ setSaving(true);
71
+ setNotice(null);
72
+ const failure = await failureOf(() => props.saveName(name.trim()));
73
+ setSaving(false);
74
+ if (failure !== undefined) {
75
+ setNotice(t('humanSettingsNameFailed', { message: failure }));
76
+ return;
77
+ }
78
+ setDraft(undefined);
79
+ };
80
+ const pickAvatar = async (file) => {
81
+ if (file === undefined)
82
+ return;
83
+ setNotice(null);
84
+ if (!file.type.startsWith('image/')) {
85
+ setNotice(t('humanSettingsAvatarNotImage'));
86
+ return;
87
+ }
88
+ if (file.size > AVATAR_MAX_BYTES) {
89
+ setNotice(t('humanSettingsAvatarTooLarge'));
90
+ return;
91
+ }
92
+ setUploading(true);
93
+ const failure = await failureOf(() => props.uploadAvatar(file));
94
+ setUploading(false);
95
+ if (failure !== undefined)
96
+ setNotice(t('humanSettingsAvatarFailed', { message: failure }));
97
+ };
98
+ const removeAvatar = async () => {
99
+ setNotice(null);
100
+ const failure = await failureOf(() => props.removeAvatar());
101
+ if (failure !== undefined)
102
+ setNotice(t('humanSettingsAvatarFailed', { message: failure }));
103
+ };
104
+ // The page says what it is before it says what happened: the settings nav
105
+ // lists it beside the Harness's own pages, so the title alone leaves "whose
106
+ // Team is this, and where does a change apply?" unanswered — and every state,
107
+ // including a failed read, has to answer it.
108
+ const pageHeader = _jsxs(_Fragment, { children: [_jsx("h2", { className: css.heading, children: t('teamSettingsTitle') }), _jsx("p", { className: css.intro, children: t('teamSettingsIntro') })] });
109
+ if (profile.status === 'loading' && profile.name === undefined) {
110
+ return _jsxs("div", { className: css.section, children: [pageHeader, _jsx("p", { className: css.state, role: "status", children: t('humanSettingsLoading') })] });
111
+ }
112
+ if (profile.name === undefined) {
113
+ return _jsxs("div", { className: css.section, children: [pageHeader, _jsx("p", { className: css.state, role: "alert", children: t('humanSettingsUnavailable', { message: profile.error ?? '' }) }), _jsx("div", { className: css.stateAction, children: _jsx(Button, { variant: "outline", onClick: () => { void identity.refresh(); }, children: t('retry') }) })] });
114
+ }
115
+ return _jsxs("div", { className: css.section, children: [pageHeader, _jsxs("div", { className: css.rows, children: [_jsxs("div", { className: css.row, children: [_jsxs("div", { className: css.rowText, children: [_jsx("div", { className: css.title, children: t('humanSettingsName') }), _jsx("div", { className: css.desc, children: t('humanSettingsNameHint') })] }), _jsxs("form", { className: css.controls, onSubmit: (event) => {
116
+ event.preventDefault();
117
+ void submitName();
118
+ }, children: [_jsx(Input, { className: css.nameInput, "aria-label": t('humanSettingsName'), "aria-invalid": emptyName || undefined, value: name, disabled: saving, onChange: (event) => { setDraft(event.target.value); } }), _jsx(Button, { type: "submit", variant: "primary", disabled: saving || !dirty || emptyName, children: saving ? t('humanSettingsSaving') : t('humanSettingsSave') })] })] }), _jsxs("div", { className: css.row, children: [_jsxs("div", { className: css.rowText, children: [_jsx("div", { className: css.title, children: t('humanSettingsAvatar') }), _jsx("div", { className: css.desc, children: t('humanSettingsAvatarHint') })] }), _jsxs("div", { className: css.controls, children: [avatar.src === undefined
119
+ ? _jsx("span", { className: `${css.identity} ${css.identityFallback}`, "data-avatar": "initial", "aria-hidden": "true", children: avatarInitial(profile.name) })
120
+ : _jsx("img", { className: `${css.identity} ${css.identityImage}`, "data-avatar": "image", src: avatar.src, alt: "", onError: avatar.failed }), _jsx("input", { ref: filePicker, type: "file", accept: "image/*", tabIndex: -1, "aria-hidden": "true", hidden: true, onChange: (event) => {
121
+ void pickAvatar(event.target.files?.[0]);
122
+ event.target.value = '';
123
+ } }), _jsx(Button, { variant: "outline", disabled: uploading, onClick: () => { filePicker.current?.click(); }, children: uploading ? t('humanSettingsUploading') : hasAvatar ? t('humanSettingsReplace') : t('humanSettingsUpload') }), hasAvatar && _jsx(Button, { disabled: uploading, onClick: () => { void removeAvatar(); }, children: t('humanSettingsRemoveAvatar') })] })] })] }), judgeForm === undefined
124
+ ? null
125
+ : _jsx(JudgeGroup, { t: t, form: judgeForm, judge: props.judge, keyConfigured: props.keyConfigured }), _jsx("div", { className: css.environment, children: _jsx(EnvironmentCheck, { t: t, environment: props.environment }) }), _jsxs("div", { className: css.footnote, children: [_jsx("span", { children: t('humanSettingsVersion', { version: profile.version ?? '' }) }), _jsx("span", { "aria-hidden": "true", children: "\u00B7" }), _jsx("a", { className: css.link, href: profile.repoUrl ?? '', target: "_blank", rel: "noreferrer", children: "GitHub" }), profile.updateAvailable && profile.latestVersion !== undefined
126
+ ? _jsx("a", { className: css.link, href: `${profile.repoUrl ?? ''}/releases`, target: "_blank", rel: "noreferrer", children: t('humanSettingsUpdateAvailable', { version: profile.latestVersion }) })
127
+ : null] }), emptyName && _jsx("p", { className: css.notice, children: t('humanSettingsNameEmpty') }), notice === null ? null : _jsx("p", { className: css.notice, role: "alert", children: notice })] });
128
+ }
129
+ /**
130
+ * The judge's endpoint as one collapsed row: the state of jev on the line
131
+ * itself, and the three controls that decide it behind a disclosure.
132
+ *
133
+ * Collapsed is the default because the endpoint is set once and then left
134
+ * alone, while its state is worth seeing every time the page opens — so the
135
+ * row carries the answer and the controls wait. The status is read rather than
136
+ * derived, because a deployment may mount a judge from configuration this page
137
+ * cannot see, and it states what the Host answered rather than predicting it.
138
+ */
139
+ function JudgeGroup(props) {
140
+ const { t, form, judge, keyConfigured } = props;
141
+ const { report } = useContextJudge(judge);
142
+ const state = useSyncExternalStore(form.subscribe, form.getSnapshot, form.getSnapshot);
143
+ const actions = form.actions();
144
+ const [open, setOpen] = useState(false);
145
+ const [clearing, setClearing] = useState(false);
146
+ const [clearFailed, setClearFailed] = useState(false);
147
+ const configured = keyConfigured();
148
+ // Every control answers to the document: a read-only document disables them
149
+ // all rather than letting a save fail after the fact.
150
+ const disabled = !state.shell.writable;
151
+ const labels = {
152
+ overridden: t('teamSettingsOverridden'),
153
+ reset: t('teamSettingsReset'),
154
+ invalid: t('teamSettingsInvalid'),
155
+ };
156
+ const field = (name) => state.fields[name];
157
+ const clear = async () => {
158
+ setClearing(true);
159
+ setClearFailed(false);
160
+ const landed = await form.clearKey();
161
+ setClearing(false);
162
+ if (!landed)
163
+ setClearFailed(true);
164
+ };
165
+ return _jsxs("div", { className: css.group, children: [_jsxs("button", { type: "button", className: css.groupToggle, "aria-expanded": open, onClick: () => { setOpen(value => !value); }, children: [_jsx("span", { className: css.groupChevron, "aria-hidden": "true", children: open ? _jsx(IconChevronDownOutlineRegular, { size: 14 }) : _jsx(IconChevronRightOutlineRegular, { size: 14 }) }), _jsx("span", { className: css.groupTitle, children: t('teamJudgeGroupTitle') }), _jsx(JudgeStatus, { t: t, report: report })] }), open
166
+ ? _jsxs("div", { className: css.groupBody, role: "group", "aria-label": t('teamJudgeGroupTitle'), "data-team-judge": true, children: [_jsx("p", { className: css.groupHint, children: t('teamJudgeGroupHint') }), _jsxs(SettingsForm, { labels: {
167
+ unavailable: t('teamSettingsUnavailable'),
168
+ readOnly: t('teamSettingsReadOnly'),
169
+ saveFailed: t('teamSettingsSaveFailed'),
170
+ save: t('teamSettingsSave'),
171
+ saving: t('teamSettingsSaving'),
172
+ }, state: state.shell, onSave: actions.save, onDiscard: actions.discard, children: [_jsx(SettingsValueField, { id: "team-judge-api-base", label: t('teamJudgeApiBase'), text: field('apiBase').text, overridden: field('apiBase').overridden, invalid: field('apiBase').invalid, overriddenLabel: labels.overridden, resetLabel: labels.reset, invalidLabel: labels.invalid, disabled: disabled, onEdit: text => { actions.edit('apiBase', text); }, onReset: () => { actions.resetField('apiBase'); } }), _jsx(SettingsValueField, { id: "team-judge-model", label: t('teamJudgeModel'), text: field('model').text, overridden: field('model').overridden, invalid: field('model').invalid, overriddenLabel: labels.overridden, resetLabel: labels.reset, invalidLabel: labels.invalid, disabled: disabled, onEdit: text => { actions.edit('model', text); }, onReset: () => { actions.resetField('model'); } }), _jsx(SettingsSecretField, { id: "team-judge-api-key", label: t('teamJudgeKey'), hint: t('teamJudgeKeyHint'), text: field('apiKey').text, configured: configured, stateLabel: configured ? t('teamJudgeKeySet') : t('teamJudgeKeyUnset'), disabled: disabled, onEdit: text => { actions.edit('apiKey', text); } }), configured
173
+ ? _jsxs("div", { className: css.clearRow, children: [_jsx(Button, { variant: "outline", size: "sm", disabled: disabled || state.shell.dirty || clearing, onClick: () => { void clear(); }, children: t('teamJudgeClearKey') }), _jsx("p", { className: css.clearHint, children: state.shell.dirty ? t('teamJudgeClearBlocked') : t('teamJudgeClearHint') }), clearFailed ? _jsx("p", { className: css.notice, role: "status", children: t('teamJudgeClearFailed') }) : null] })
174
+ : null] })] })
175
+ : null] });
176
+ }
177
+ /** The three glyphs, drawn at the 16px seat the settings page uses for state marks. */
178
+ const ICONS = {
179
+ ok: _jsx("path", { d: "M3 8.5 6.5 12 13 4.5" }),
180
+ off: _jsxs(_Fragment, { children: [_jsx("circle", { cx: "8", cy: "8", r: "6" }), _jsx("path", { d: "M5.6 8h4.8" })] }),
181
+ warn: _jsxs(_Fragment, { children: [_jsx("path", { d: "M8 2.5 14.6 13.6H1.4z" }), _jsx("path", { d: "M8 6.6v3.2" }), _jsx("path", { d: "M8 11.9h.01" })] }),
182
+ };
183
+ /**
184
+ * The judge's live status in one line, inside the row that carries it. The
185
+ * verdict is stated as text and an icon, never by color alone, and nothing is
186
+ * said before the first read settles: the row then stands as a bare title.
187
+ */
188
+ function JudgeStatus({ t, report }) {
189
+ if (report === undefined)
190
+ return null;
191
+ const verdict = report.enabled ? 'enabled' : report.reason ?? 'unavailable';
192
+ return _jsxs("span", { className: css.status, "data-context-judge": verdict, children: [_jsx("svg", { className: css.statusIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: ICONS[report.enabled ? 'ok' : report.reason === 'no-key' ? 'off' : 'warn'] }), _jsx("span", { children: report.enabled
193
+ ? t('teamJudgeReady')
194
+ : report.reason === 'no-key' ? t('teamJudgeNoKey') : t('teamJudgeUnavailable') })] });
195
+ }
@@ -1,16 +1,13 @@
1
+ import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
1
2
  import type { AgentTeamAttachmentId, AgentTeamGetAttachmentRequest, AgentTeamGetAttachmentResult } from '@wowyuarm/dsh-agent-team/types';
2
3
  /** Base64 one file payload in chunks so large uploads stay off the call-stack limit. */
3
4
  export declare function bytesToBase64(bytes: Uint8Array): string;
4
- /** Mirrors the slot's Remote result union without importing the slots module. */
5
- type GetAttachment = (request: AgentTeamGetAttachmentRequest) => Promise<{
6
- ok: true;
7
- value: AgentTeamGetAttachmentResult;
8
- } | {
9
- ok: false;
10
- error: {
11
- message: string;
12
- };
13
- }>;
5
+ /**
6
+ * The slot's Remote result union without importing the slots module: an
7
+ * expected failure arrives as a stable `code`, and any other error stays
8
+ * unknown to this cache.
9
+ */
10
+ export type GetAttachment = (request: AgentTeamGetAttachmentRequest) => Promise<RemoteResult<AgentTeamGetAttachmentResult>>;
14
11
  export declare function cachedAttachmentDataUrl(attachmentId: AgentTeamAttachmentId): string | null | undefined;
15
12
  export declare function loadAttachmentDataUrl(getAttachment: GetAttachment, attachment: {
16
13
  attachmentId: AgentTeamAttachmentId;
@@ -18,5 +15,4 @@ export declare function loadAttachmentDataUrl(getAttachment: GetAttachment, atta
18
15
  }): Promise<string | null>;
19
16
  /** Human-readable byte size for attachment chips. */
20
17
  export declare function formatByteSize(byteSize: number): string;
21
- export {};
22
18
  //# sourceMappingURL=attachment-preview.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"attachment-preview.d.ts","sourceRoot":"","sources":["../../../src/client/attachment-preview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,6BAA6B,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAA;AAExI,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAOvD;AAED,iFAAiF;AACjF,KAAK,aAAa,GAAG,CAAC,OAAO,EAAE,6BAA6B,KAAK,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,4BAA4B,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC,CAAA;AASvK,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,qBAAqB,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAEtG;AAED,wBAAsB,qBAAqB,CAAC,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE;IAAE,YAAY,EAAE,qBAAqB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOxK;AAED,qDAAqD;AACrD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIvD"}
1
+ {"version":3,"file":"attachment-preview.d.ts","sourceRoot":"","sources":["../../../src/client/attachment-preview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AACpE,OAAO,KAAK,EAAE,qBAAqB,EAAE,6BAA6B,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAA;AAExI,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAOvD;AAED;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,6BAA6B,KAAK,OAAO,CAAC,YAAY,CAAC,4BAA4B,CAAC,CAAC,CAAA;AAW3H,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,qBAAqB,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAEtG;AAED,wBAAsB,qBAAqB,CAAC,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE;IAAE,YAAY,EAAE,qBAAqB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAcxK;AAED,qDAAqD;AACrD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIvD"}
@@ -9,8 +9,10 @@ export function bytesToBase64(bytes) {
9
9
  }
10
10
  /**
11
11
  * Thumbnail data URLs live in one session-wide cache: message lists re-render
12
- * often, and each miss costs a Host round-trip plus a base64 decode. Failed
13
- * loads (bytes already GC'd) are cached as `null` so the chip fallback is stable.
12
+ * often, and each miss costs a Host round-trip plus a base64 decode. Only a
13
+ * settled answer is cached: a gone entry resolves to `null` so the chip stays
14
+ * on the expired label, while an unknown failure caches nothing and lets a
15
+ * later mount retry the Host.
14
16
  */
15
17
  const dataUrlCache = new Map();
16
18
  export function cachedAttachmentDataUrl(attachmentId) {
@@ -21,7 +23,15 @@ export async function loadAttachmentDataUrl(getAttachment, attachment) {
21
23
  if (cached !== undefined)
22
24
  return cached;
23
25
  const result = await getAttachment({ attachmentId: attachment.attachmentId });
24
- const url = result.ok && attachment.mediaType.startsWith('image/') ? `data:${attachment.mediaType};base64,${result.value.bytesBase64}` : null;
26
+ if (!result.ok) {
27
+ // Discriminate by code, never by message: the stable not-found code is a
28
+ // settled fact, everything else (carrier, Host, unknown future codes) is
29
+ // transient from this cache's point of view.
30
+ if (result.error.code === 'team/attachment-not-found')
31
+ dataUrlCache.set(attachment.attachmentId, null);
32
+ return null;
33
+ }
34
+ const url = attachment.mediaType.startsWith('image/') ? `data:${attachment.mediaType};base64,${result.value.bytesBase64}` : null;
25
35
  dataUrlCache.set(attachment.attachmentId, url);
26
36
  return url;
27
37
  }
@@ -0,0 +1,64 @@
1
+ import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
2
+ import type { AgentTeamContextJudgeResult } from '@wowyuarm/dsh-agent-team/types';
3
+ /**
4
+ * The Client's one projection of the long-gap gate's judge.
5
+ *
6
+ * The settings page stages the row configuration that mounts the judge, but it
7
+ * cannot state whether a judge is actually reachable: `enabled` is a fact about
8
+ * the running Host, and a deployment may mount one from configuration the page
9
+ * never sees. So this projection is read rather than derived, and every write
10
+ * that can mount or unmount the judge re-reads it instead of predicting the
11
+ * outcome.
12
+ *
13
+ * Reads are demand-driven off the first subscriber, so an ordinary conversation
14
+ * never calls the Remote.
15
+ */
16
+ /** One read of the judge status, replaced wholesale on every change. */
17
+ export interface TeamContextJudgeSnapshot {
18
+ /** `loading` until the first read settles, `ready` once it has. */
19
+ readonly status: 'loading' | 'ready';
20
+ /** The status to render; undefined until the first read settles. */
21
+ readonly report?: AgentTeamContextJudgeResult | undefined;
22
+ }
23
+ /** Read-side face the settings page binds. */
24
+ export interface TeamContextJudgeSource {
25
+ getSnapshot(): TeamContextJudgeSnapshot;
26
+ subscribe(listener: () => void): () => void;
27
+ }
28
+ /** Host call the projection reads through; one loader per Client context. */
29
+ export interface TeamContextJudgeLoader {
30
+ loadContextJudge: () => Promise<RemoteResult<AgentTeamContextJudgeResult>>;
31
+ }
32
+ export declare class TeamContextJudgeCheck implements TeamContextJudgeSource {
33
+ private snapshot;
34
+ private readonly listeners;
35
+ private reading;
36
+ private readonly loader;
37
+ constructor(loader: TeamContextJudgeLoader);
38
+ readonly getSnapshot: () => TeamContextJudgeSnapshot;
39
+ /**
40
+ * Observe the status, starting the first read when nobody has read yet.
41
+ * @param listener - invoked after every snapshot replacement.
42
+ * @returns the disposer removing this listener.
43
+ */
44
+ readonly subscribe: (listener: () => void) => (() => void);
45
+ /**
46
+ * Read the Host status. Concurrent callers share one round trip, and a failed
47
+ * read keeps the last accepted report — the block never blanks out over a
48
+ * background read.
49
+ * @returns settlement of this read (or of the read already in flight).
50
+ */
51
+ refresh(): Promise<void>;
52
+ dispose(): void;
53
+ private read;
54
+ /**
55
+ * Settle a read that produced no report, so a reader stops waiting on a
56
+ * `loading` that will never finish. A report that already stands is left
57
+ * untouched: a failed background read never blanks the block.
58
+ */
59
+ private fail;
60
+ private commit;
61
+ }
62
+ /** Subscribe one rendered block to the judge status. */
63
+ export declare function useContextJudge(judge: TeamContextJudgeSource): TeamContextJudgeSnapshot;
64
+ //# sourceMappingURL=context-judge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-judge.d.ts","sourceRoot":"","sources":["../../../src/client/context-judge.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AACpE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,gCAAgC,CAAA;AAEjF;;;;;;;;;;;;GAYG;AAEH,wEAAwE;AACxE,MAAM,WAAW,wBAAwB;IACvC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAA;IACpC,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,2BAA2B,GAAG,SAAS,CAAA;CAC1D;AAED,8CAA8C;AAC9C,MAAM,WAAW,sBAAsB;IACrC,WAAW,IAAI,wBAAwB,CAAA;IACvC,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;CAC5C;AAED,6EAA6E;AAC7E,MAAM,WAAW,sBAAsB;IACrC,gBAAgB,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,2BAA2B,CAAC,CAAC,CAAA;CAC3E;AAID,qBAAa,qBAAsB,YAAW,sBAAsB;IAClE,OAAO,CAAC,QAAQ,CAAoC;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAClD,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;gBAEnC,MAAM,EAAE,sBAAsB;IAI1C,QAAQ,CAAC,WAAW,QAAO,wBAAwB,CAAiB;IAEpE;;;;OAIG;IACH,QAAQ,CAAC,SAAS,GAAI,UAAU,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC,CAIxD;IAED;;;;;OAKG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IASxB,OAAO,IAAI,IAAI;YAID,IAAI;IAkBlB;;;;OAIG;IACH,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,MAAM;CAIf;AAED,wDAAwD;AACxD,wBAAgB,eAAe,CAAC,KAAK,EAAE,sBAAsB,GAAG,wBAAwB,CAEvF"}
@@ -0,0 +1,78 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ const INITIAL = { status: 'loading' };
3
+ export class TeamContextJudgeCheck {
4
+ snapshot = INITIAL;
5
+ listeners = new Set();
6
+ reading;
7
+ loader;
8
+ constructor(loader) {
9
+ this.loader = loader;
10
+ }
11
+ getSnapshot = () => this.snapshot;
12
+ /**
13
+ * Observe the status, starting the first read when nobody has read yet.
14
+ * @param listener - invoked after every snapshot replacement.
15
+ * @returns the disposer removing this listener.
16
+ */
17
+ subscribe = (listener) => {
18
+ this.listeners.add(listener);
19
+ if (this.snapshot.status === 'loading' && this.reading === undefined)
20
+ void this.refresh();
21
+ return () => { this.listeners.delete(listener); };
22
+ };
23
+ /**
24
+ * Read the Host status. Concurrent callers share one round trip, and a failed
25
+ * read keeps the last accepted report — the block never blanks out over a
26
+ * background read.
27
+ * @returns settlement of this read (or of the read already in flight).
28
+ */
29
+ refresh() {
30
+ if (this.reading !== undefined)
31
+ return this.reading;
32
+ const reading = this.read().finally(() => {
33
+ if (this.reading === reading)
34
+ this.reading = undefined;
35
+ });
36
+ this.reading = reading;
37
+ return reading;
38
+ }
39
+ dispose() {
40
+ this.listeners.clear();
41
+ }
42
+ async read() {
43
+ let report;
44
+ try {
45
+ const result = await this.loader.loadContextJudge();
46
+ if (!result.ok) {
47
+ this.fail();
48
+ return;
49
+ }
50
+ report = result.value;
51
+ }
52
+ catch {
53
+ // A dropped connection surfaces as a thrown carrier error, not a result:
54
+ // both are read failures and both keep whatever report already stands.
55
+ this.fail();
56
+ return;
57
+ }
58
+ this.commit({ status: 'ready', report });
59
+ }
60
+ /**
61
+ * Settle a read that produced no report, so a reader stops waiting on a
62
+ * `loading` that will never finish. A report that already stands is left
63
+ * untouched: a failed background read never blanks the block.
64
+ */
65
+ fail() {
66
+ if (this.snapshot.report === undefined)
67
+ this.commit({ status: 'ready' });
68
+ }
69
+ commit(snapshot) {
70
+ this.snapshot = snapshot;
71
+ for (const listener of this.listeners)
72
+ listener();
73
+ }
74
+ }
75
+ /** Subscribe one rendered block to the judge status. */
76
+ export function useContextJudge(judge) {
77
+ return useSyncExternalStore(judge.subscribe, judge.getSnapshot, judge.getSnapshot);
78
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AA8CnE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAK5C,OAAO,EAAU,KAAK,OAAO,EAAE,MAAM,cAAc,CAAA;AAEnD,YAAY,EAAE,QAAQ,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAC9F,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAIhD,eAAO,MAAM,MAAM,UAElB,CAAA;AAuBD,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,IAAI,EAAE,OAAO,CAAA;KACd;CACF;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,cAAc,EAAE,cAAc,CAAA;QAC9B,UAAU,EAAE,cAAc,CAAA;KAC3B;CACF;AA8QD,wBAAsB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAgDnE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAK5C,OAAO,EAAU,KAAK,OAAO,EAAE,MAAM,cAAc,CAAA;AAEnD,YAAY,EAAE,QAAQ,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAC9F,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAYhD,eAAO,MAAM,MAAM,UAElB,CAAA;AAuBD,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,IAAI,EAAE,OAAO,CAAA;KACd;CACF;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,cAAc,EAAE,cAAc,CAAA;QAC9B,UAAU,EAAE,cAAc,CAAA;KAC3B;CACF;AAoUD,wBAAsB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
@@ -1,5 +1,7 @@
1
1
  import agentTeamRemote from '@wowyuarm/dsh-agent-team/remote';
2
- import { HumanSettingsSection } from "./HumanSettingsSection.js";
2
+ import { TeamSettingsSection } from "./TeamSettingsSection.js";
3
+ import { TeamContextJudgeCheck } from "./context-judge.js";
4
+ import { TeamJudgeForm, TeamJudgeFormSeat } from "./judge-form.js";
3
5
  import { TeamHumanIdentity } from "./human-identity.js";
4
6
  import { TeamEnvironmentCheck } from "./environment-check.js";
5
7
  import { bytesToBase64 } from "./attachment-preview.js";
@@ -13,8 +15,15 @@ import { TeamWorkspaceBrowser } from "./TeamWorkspaceBrowser.js";
13
15
  import { en, zh } from "./locales.js";
14
16
  export { TeamNavigation } from "./navigation.js";
15
17
  const NS = 'team';
18
+ /**
19
+ * The Team Host row's settings namespace. Spelled here rather than imported,
20
+ * because a client package must not depend on a Host package — the profile, the
21
+ * judge's endpoint, and the gate's thresholds all live in this one row section,
22
+ * and each settings page renders only the group it owns.
23
+ */
24
+ const TEAM_SETTINGS_NS = 'wowyuarm-agent-team-host';
16
25
  export const inject = [
17
- 'slots', 'workspaces', 'locale', 'remote', 'remote.session', 'sessions', 'connection', 'conversation', 'uiWorkspace',
26
+ 'slots', 'workspaces', 'layout', 'locale', 'remote', 'remote.session', 'sessions', 'connection', 'conversation', 'uiWorkspace',
18
27
  ];
19
28
  /**
20
29
  * 0.1.7 moved the conversation selection into the workspace service: the
@@ -167,11 +176,25 @@ function applyUi(ctx) {
167
176
  const environment = new TeamEnvironmentCheck({
168
177
  loadEnvironment: () => ctx.remote.agentTeam.environment({}),
169
178
  });
179
+ // The long-gap gate's judge: what the Host answers about reachability, and
180
+ // the thresholds in force. Its own projection rather than a field of the
181
+ // environment check, because it re-reads after every write that can mount or
182
+ // unmount the judge while the environment verdict is settled once.
183
+ const contextJudge = new TeamContextJudgeCheck({
184
+ loadContextJudge: () => ctx.remote.agentTeam.contextJudge({}),
185
+ });
186
+ // The settings page's endpoint form. It is held in a seat rather than handed
187
+ // over as a value, because the service it is built over may arrive after the
188
+ // page's injected props were computed — and a slot entry's props are computed
189
+ // once and cached, so a value would freeze the answer.
190
+ const judgeForm = new TeamJudgeFormSeat();
170
191
  ctx.effect(() => () => {
171
192
  navigation.dispose();
172
193
  drafts.dispose();
173
194
  humanIdentity.dispose();
174
195
  environment.dispose();
196
+ contextJudge.dispose();
197
+ judgeForm.dispose();
175
198
  void disposeNavigation();
176
199
  void disposeDrafts();
177
200
  }, 'agent-team: navigation service');
@@ -203,6 +226,25 @@ function applyUi(ctx) {
203
226
  restore();
204
227
  };
205
228
  }, 'agent-team: member session restore');
229
+ // The shell renders one keyed main panel at a time — `activePanelId ?? 'conversation'`
230
+ // — so a shipped global panel (the plugin manager) stays on screen after Team mode is
231
+ // entered, and every Team destination then opens behind it: the sidebar moves while the
232
+ // column keeps rendering the foreign page. Team mode owns the conversation seat, so
233
+ // entering it, or navigating inside it, hands the column back to that seat. A null
234
+ // selection — and every mode but Team, where panel choice belongs to the shipped
235
+ // sidebar — is left untouched, so this never fights the shell's own selection.
236
+ ctx.effect(() => {
237
+ const releaseMainPanel = () => {
238
+ if (navigation.getSnapshot().mode !== 'team')
239
+ return;
240
+ if (ctx.layout.panelInfo.getSnapshot().activePanelId === null)
241
+ return;
242
+ ctx.layout.selectPanel(null);
243
+ };
244
+ const unsubscribe = navigation.subscribe(releaseMainPanel);
245
+ releaseMainPanel();
246
+ return unsubscribe;
247
+ }, 'agent-team: main panel ownership');
206
248
  const changes = new TeamChangeStream(ctx.remote);
207
249
  ctx.effect(() => () => changes.dispose(), 'agent-team: change subscriptions');
208
250
  // The Harness resumes a live generation across reconnects, but a stream that
@@ -244,21 +286,43 @@ function applyUi(ctx) {
244
286
  registerModeShadow(ctx, navigation, changes, reads, drafts, humanIdentity, 'sidebar.workspaces', TeamWorkspaceBrowser);
245
287
  registerModeShadow(ctx, navigation, changes, reads, drafts, humanIdentity, 'main', TeamConversation, undefined, 'conversation');
246
288
  registerModeShadow(ctx, navigation, changes, reads, drafts, humanIdentity, 'sidebar.settings', TeamMembersAction, () => ({ loadMemberGroups }));
247
- // The Human profile page: one settings section, ordered between General (0)
248
- // and Models (10) so identity sits near the top. Writes go through the Team
249
- // Remote, which answers with the Host's own rejection reason (a name
250
- // collides, an empty one), and the shared identity re-reads afterwards, so a
251
- // rename lands in the timeline and the member refs at the same moment the
252
- // page shows it.
289
+ // The Team's settings page: one settings section, ordered between General (0)
290
+ // and Models (10) so it sits near the top. It holds two groups that write
291
+ // through two different documents: identity goes through the Team Remote,
292
+ // which answers with the Host's own rejection reason (a name collides, an
293
+ // empty one), and the shared identity re-reads afterwards, so a rename lands
294
+ // in the timeline and the member refs at the same moment the page shows it.
295
+ //
296
+ // The judge's endpoint is one group of the Team row's settings section, so it
297
+ // is offered only while this deployment serves a settings document: the page
298
+ // is registered here regardless, because identity is reachable without one,
299
+ // and the seat above stays empty when there is none. The service is read
300
+ // through `ctx.get` rather than a hard `inject` for the same reason — a
301
+ // deployment without the settings UI keeps the rest of the Team.
302
+ ctx.inject(['configForms'], (scope) => {
303
+ const form = new TeamJudgeForm(scope.configForms.get(TEAM_SETTINGS_NS), contextJudge);
304
+ judgeForm.hold(form);
305
+ scope.effect(() => () => {
306
+ judgeForm.release(form);
307
+ form.dispose();
308
+ }, 'agent-team: judge settings form');
309
+ });
253
310
  ctx.slots.inject('settings.section', () => ctx.slots.register({
254
311
  name: 'settings.section',
255
- id: 'team-human',
312
+ id: 'team-settings',
256
313
  order: 5,
257
- label: () => ctx.locale.bind(NS)('humanSettingsNav'),
314
+ label: () => ctx.locale.bind(NS)('teamSettingsNav'),
258
315
  locale: NS,
259
316
  inject: () => ({
260
317
  identity: humanIdentity,
261
318
  environment,
319
+ judge: contextJudge,
320
+ judgeForm,
321
+ // The key's literal never rides a response; the describe mirror is what
322
+ // says whether one is stored.
323
+ keyConfigured: () => ctx.get('configForms')?.describe().getSnapshot().view?.namespaces
324
+ .find(row => row.ns === TEAM_SETTINGS_NS)?.secrets
325
+ .some(secret => secret.path.join('.') === 'jev.apiKey' && secret.set) ?? false,
262
326
  saveName: async (name) => {
263
327
  const saved = await ctx.remote.agentTeam.setHumanProfile({ name });
264
328
  if (!saved.ok)
@@ -294,7 +358,7 @@ function applyUi(ctx) {
294
358
  return undefined;
295
359
  },
296
360
  }),
297
- }, HumanSettingsSection));
361
+ }, TeamSettingsSection));
298
362
  }
299
363
  export async function apply(ctx) {
300
364
  const disposeRemote = await ctx.remote.$mount(agentTeamRemote);