@wowyuarm/dsh-agent-team 0.1.15 → 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.
- package/README.md +8 -4
- package/README.zh.md +8 -4
- package/cordis.patch.yml +26 -12
- package/package.json +52 -47
- package/packages/agent-team/lib/attachments.js +63 -4
- package/packages/agent-team/lib/compaction-engine.js +18 -0
- package/packages/agent-team/lib/context-continuity-host.js +198 -4
- package/packages/agent-team/lib/context-judge.js +150 -0
- package/packages/agent-team/lib/context-source.js +11 -6
- package/packages/agent-team/lib/environment-check.js +102 -0
- package/packages/agent-team/lib/human-profile.js +23 -37
- package/packages/agent-team/lib/index.js +510 -99
- package/packages/agent-team/lib/installed-manifest.js +32 -0
- package/packages/agent-team/lib/invariant.js +36 -12
- package/packages/agent-team/lib/ledger.js +37 -1
- package/packages/agent-team/lib/member-context.js +2 -4
- package/packages/agent-team/lib/member-runtime.js +2 -1
- package/packages/agent-team/lib/member-time-context.js +2 -4
- package/packages/agent-team/lib/recovery.js +53 -4
- package/packages/agent-team/lib/typert.host.js +225 -54
- package/packages/agent-team/lib/typert.remote-client.d.ts +5 -1
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +127 -39
- package/packages/agent-team/lib/types/attachments.d.ts +32 -2
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
- package/packages/agent-team/lib/types/compaction-engine.d.ts +19 -0
- package/packages/agent-team/lib/types/compaction-engine.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-continuity-host.d.ts +131 -4
- package/packages/agent-team/lib/types/context-continuity-host.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-judge.d.ts +82 -0
- package/packages/agent-team/lib/types/context-judge.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-source.d.ts +7 -4
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
- package/packages/agent-team/lib/types/environment-check.d.ts +66 -0
- package/packages/agent-team/lib/types/environment-check.d.ts.map +1 -0
- package/packages/agent-team/lib/types/human-profile.d.ts +21 -27
- package/packages/agent-team/lib/types/human-profile.d.ts.map +1 -1
- package/packages/agent-team/lib/types/index.d.ts +281 -18
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/installed-manifest.d.ts +16 -0
- package/packages/agent-team/lib/types/installed-manifest.d.ts.map +1 -0
- package/packages/agent-team/lib/types/invariant.d.ts +27 -5
- package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +12 -0
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts +1 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/recovery.d.ts +19 -6
- package/packages/agent-team/lib/types/recovery.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/entities.d.ts +1 -8
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +94 -2
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types.d.ts +9 -0
- package/packages/agent-team/lib/types/types.d.ts.map +1 -1
- package/packages/client-agent-team/lib/client.js +1264 -383
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/EnvironmentCheck.d.ts +18 -0
- package/packages/client-agent-team/lib/types/client/EnvironmentCheck.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/EnvironmentCheck.js +58 -0
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +0 -5
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +2 -7
- package/packages/client-agent-team/lib/types/client/TeamSettingsSection.d.ts +26 -0
- package/packages/client-agent-team/lib/types/client/TeamSettingsSection.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamSettingsSection.js +195 -0
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +7 -11
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/attachment-preview.js +13 -3
- package/packages/client-agent-team/lib/types/client/context-judge.d.ts +64 -0
- package/packages/client-agent-team/lib/types/client/context-judge.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/context-judge.js +78 -0
- package/packages/client-agent-team/lib/types/client/environment-check.d.ts +65 -0
- package/packages/client-agent-team/lib/types/client/environment-check.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/environment-check.js +78 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +84 -11
- package/packages/client-agent-team/lib/types/client/judge-form.d.ts +99 -0
- package/packages/client-agent-team/lib/types/client/judge-form.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/judge-form.js +208 -0
- package/packages/client-agent-team/lib/types/client/locales.d.ts +70 -14
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +76 -14
- package/packages/tool-agent-team/README.md +6 -3
- package/packages/tool-agent-team/README.zh.md +6 -3
- package/packages/tool-agent-team/lib/context-tools.js +148 -164
- package/packages/tool-agent-team/lib/index.js +17 -2
- package/packages/tool-agent-team/lib/types/context-tools.d.ts +32 -7
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/pressure-policy.js +0 -230
- package/packages/agent-team/lib/types/pressure-policy.d.ts +0 -130
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts +0 -15
- package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/HumanSettingsSection.js +0 -115
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
|
|
2
|
+
import type { AgentTeamEnvironmentResult } from '@wowyuarm/dsh-agent-team/types';
|
|
3
|
+
/**
|
|
4
|
+
* The Client's one projection of the local environment check.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately separate from the Human identity projection: this is a fact
|
|
7
|
+
* about the installation, not about the Human, it is read once and never
|
|
8
|
+
* written back, and the settings page is the only surface that renders it.
|
|
9
|
+
* Folding it into the identity store would make every seat that names the
|
|
10
|
+
* Human depend on a version comparison it has no use for.
|
|
11
|
+
*
|
|
12
|
+
* Reads are demand-driven off the first subscriber, so an ordinary conversation
|
|
13
|
+
* never calls the Remote. There is no retry: `undetermined` is a settled
|
|
14
|
+
* verdict about facts the Host could not establish, not a failed request, so a
|
|
15
|
+
* reader has nothing to retry.
|
|
16
|
+
*/
|
|
17
|
+
/** One read of the environment check, replaced wholesale on every change. */
|
|
18
|
+
export interface TeamEnvironmentSnapshot {
|
|
19
|
+
/** `loading` until the first read settles, `ready` once it has. */
|
|
20
|
+
readonly status: 'loading' | 'ready';
|
|
21
|
+
/** The report to render; undefined until the first read settles. */
|
|
22
|
+
readonly report?: AgentTeamEnvironmentResult | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Read-side face the settings page binds. */
|
|
25
|
+
export interface TeamEnvironmentSource {
|
|
26
|
+
getSnapshot(): TeamEnvironmentSnapshot;
|
|
27
|
+
subscribe(listener: () => void): () => void;
|
|
28
|
+
}
|
|
29
|
+
/** Host call the store reads through; one loader per Client context. */
|
|
30
|
+
export interface TeamEnvironmentLoader {
|
|
31
|
+
loadEnvironment: () => Promise<RemoteResult<AgentTeamEnvironmentResult>>;
|
|
32
|
+
}
|
|
33
|
+
export declare class TeamEnvironmentCheck implements TeamEnvironmentSource {
|
|
34
|
+
private snapshot;
|
|
35
|
+
private readonly listeners;
|
|
36
|
+
private reading;
|
|
37
|
+
private readonly loader;
|
|
38
|
+
constructor(loader: TeamEnvironmentLoader);
|
|
39
|
+
readonly getSnapshot: () => TeamEnvironmentSnapshot;
|
|
40
|
+
/**
|
|
41
|
+
* Observe the check, starting the first read when nobody has read yet.
|
|
42
|
+
* @param listener - invoked after every snapshot replacement.
|
|
43
|
+
* @returns the disposer removing this listener.
|
|
44
|
+
*/
|
|
45
|
+
readonly subscribe: (listener: () => void) => (() => void);
|
|
46
|
+
/**
|
|
47
|
+
* Read the Host projection. Concurrent callers share one round trip, and a
|
|
48
|
+
* failed read keeps the last accepted report — the block never blanks out
|
|
49
|
+
* over a background read.
|
|
50
|
+
* @returns settlement of this read (or of the read already in flight).
|
|
51
|
+
*/
|
|
52
|
+
refresh(): Promise<void>;
|
|
53
|
+
dispose(): void;
|
|
54
|
+
private read;
|
|
55
|
+
/**
|
|
56
|
+
* Settle a read that produced no report, so a reader stops waiting on a
|
|
57
|
+
* `loading` that will never finish. A report that already stands is left
|
|
58
|
+
* untouched: a failed background read never blanks the block.
|
|
59
|
+
*/
|
|
60
|
+
private fail;
|
|
61
|
+
private commit;
|
|
62
|
+
}
|
|
63
|
+
/** Subscribe one rendered block to the environment check. */
|
|
64
|
+
export declare function useEnvironmentCheck(environment: TeamEnvironmentSource): TeamEnvironmentSnapshot;
|
|
65
|
+
//# sourceMappingURL=environment-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment-check.d.ts","sourceRoot":"","sources":["../../../src/client/environment-check.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AACpE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAA;AAEhF;;;;;;;;;;;;;GAaG;AAEH,6EAA6E;AAC7E,MAAM,WAAW,uBAAuB;IACtC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAA;IACpC,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,0BAA0B,GAAG,SAAS,CAAA;CACzD;AAED,8CAA8C;AAC9C,MAAM,WAAW,qBAAqB;IACpC,WAAW,IAAI,uBAAuB,CAAA;IACtC,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;CAC5C;AAED,wEAAwE;AACxE,MAAM,WAAW,qBAAqB;IACpC,eAAe,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,0BAA0B,CAAC,CAAC,CAAA;CACzE;AAID,qBAAa,oBAAqB,YAAW,qBAAqB;IAChE,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAClD,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;gBAElC,MAAM,EAAE,qBAAqB;IAIzC,QAAQ,CAAC,WAAW,QAAO,uBAAuB,CAAiB;IAEnE;;;;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,6DAA6D;AAC7D,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,qBAAqB,GAAG,uBAAuB,CAE/F"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
const INITIAL = { status: 'loading' };
|
|
3
|
+
export class TeamEnvironmentCheck {
|
|
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 check, 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 projection. Concurrent callers share one round trip, and a
|
|
25
|
+
* failed read keeps the last accepted report — the block never blanks out
|
|
26
|
+
* over a 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.loadEnvironment();
|
|
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 environment check. */
|
|
76
|
+
export function useEnvironmentCheck(environment) {
|
|
77
|
+
return useSyncExternalStore(environment.subscribe, environment.getSnapshot, environment.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;
|
|
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,6 +1,9 @@
|
|
|
1
1
|
import agentTeamRemote from '@wowyuarm/dsh-agent-team/remote';
|
|
2
|
-
import {
|
|
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";
|
|
6
|
+
import { TeamEnvironmentCheck } from "./environment-check.js";
|
|
4
7
|
import { bytesToBase64 } from "./attachment-preview.js";
|
|
5
8
|
import { TeamNavigation } from "./navigation.js";
|
|
6
9
|
import { TeamChangeStream, TeamReadStream } from "./team-changes.js";
|
|
@@ -12,8 +15,15 @@ import { TeamWorkspaceBrowser } from "./TeamWorkspaceBrowser.js";
|
|
|
12
15
|
import { en, zh } from "./locales.js";
|
|
13
16
|
export { TeamNavigation } from "./navigation.js";
|
|
14
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';
|
|
15
25
|
export const inject = [
|
|
16
|
-
'slots', 'workspaces', 'locale', 'remote', 'remote.session', 'sessions', 'connection', 'conversation', 'uiWorkspace',
|
|
26
|
+
'slots', 'workspaces', 'layout', 'locale', 'remote', 'remote.session', 'sessions', 'connection', 'conversation', 'uiWorkspace',
|
|
17
27
|
];
|
|
18
28
|
/**
|
|
19
29
|
* 0.1.7 moved the conversation selection into the workspace service: the
|
|
@@ -160,10 +170,31 @@ function applyUi(ctx) {
|
|
|
160
170
|
return `data:${result.value.mediaType};base64,${result.value.bytesBase64}`;
|
|
161
171
|
},
|
|
162
172
|
});
|
|
173
|
+
// The local environment check: a fact about this installation rather than
|
|
174
|
+
// about the Human, so it is its own projection and never a field of the
|
|
175
|
+
// identity above. Read once, on the first subscriber, and never written back.
|
|
176
|
+
const environment = new TeamEnvironmentCheck({
|
|
177
|
+
loadEnvironment: () => ctx.remote.agentTeam.environment({}),
|
|
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();
|
|
163
191
|
ctx.effect(() => () => {
|
|
164
192
|
navigation.dispose();
|
|
165
193
|
drafts.dispose();
|
|
166
194
|
humanIdentity.dispose();
|
|
195
|
+
environment.dispose();
|
|
196
|
+
contextJudge.dispose();
|
|
197
|
+
judgeForm.dispose();
|
|
167
198
|
void disposeNavigation();
|
|
168
199
|
void disposeDrafts();
|
|
169
200
|
}, 'agent-team: navigation service');
|
|
@@ -195,6 +226,25 @@ function applyUi(ctx) {
|
|
|
195
226
|
restore();
|
|
196
227
|
};
|
|
197
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');
|
|
198
248
|
const changes = new TeamChangeStream(ctx.remote);
|
|
199
249
|
ctx.effect(() => () => changes.dispose(), 'agent-team: change subscriptions');
|
|
200
250
|
// The Harness resumes a live generation across reconnects, but a stream that
|
|
@@ -236,20 +286,43 @@ function applyUi(ctx) {
|
|
|
236
286
|
registerModeShadow(ctx, navigation, changes, reads, drafts, humanIdentity, 'sidebar.workspaces', TeamWorkspaceBrowser);
|
|
237
287
|
registerModeShadow(ctx, navigation, changes, reads, drafts, humanIdentity, 'main', TeamConversation, undefined, 'conversation');
|
|
238
288
|
registerModeShadow(ctx, navigation, changes, reads, drafts, humanIdentity, 'sidebar.settings', TeamMembersAction, () => ({ loadMemberGroups }));
|
|
239
|
-
// The
|
|
240
|
-
// and Models (10) so
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
// 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
|
+
});
|
|
245
310
|
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
246
311
|
name: 'settings.section',
|
|
247
|
-
id: 'team-
|
|
312
|
+
id: 'team-settings',
|
|
248
313
|
order: 5,
|
|
249
|
-
label: () => ctx.locale.bind(NS)('
|
|
314
|
+
label: () => ctx.locale.bind(NS)('teamSettingsNav'),
|
|
250
315
|
locale: NS,
|
|
251
316
|
inject: () => ({
|
|
252
317
|
identity: humanIdentity,
|
|
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,
|
|
253
326
|
saveName: async (name) => {
|
|
254
327
|
const saved = await ctx.remote.agentTeam.setHumanProfile({ name });
|
|
255
328
|
if (!saved.ok)
|
|
@@ -285,7 +358,7 @@ function applyUi(ctx) {
|
|
|
285
358
|
return undefined;
|
|
286
359
|
},
|
|
287
360
|
}),
|
|
288
|
-
},
|
|
361
|
+
}, TeamSettingsSection));
|
|
289
362
|
}
|
|
290
363
|
export async function apply(ctx) {
|
|
291
364
|
const disposeRemote = await ctx.remote.$mount(agentTeamRemote);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { type SettingsFieldState, type SettingsFormActions, type SettingsFormScope, type SettingsFormShell } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
2
|
+
import type { TeamContextJudgeCheck } from './context-judge.ts';
|
|
3
|
+
/** Where each control's value lives inside the Team row's settings section. */
|
|
4
|
+
declare const FIELD_PATHS: {
|
|
5
|
+
readonly apiBase: readonly ["jev", "apiBase"];
|
|
6
|
+
readonly model: readonly ["jev", "model"];
|
|
7
|
+
readonly apiKey: readonly ["jev", "apiKey"];
|
|
8
|
+
};
|
|
9
|
+
/** One control this page owns. */
|
|
10
|
+
export type TeamJudgeField = keyof typeof FIELD_PATHS;
|
|
11
|
+
/** The group this form edits, as it sits inside the Team row's section. */
|
|
12
|
+
export interface TeamJudgeSection {
|
|
13
|
+
/** The judge's endpoint configuration. */
|
|
14
|
+
readonly jev?: unknown;
|
|
15
|
+
}
|
|
16
|
+
/** Everything the page renders about the form, in one replaceable value. */
|
|
17
|
+
export interface TeamJudgeFormSnapshot {
|
|
18
|
+
/** Availability, writability, and what a save would do. */
|
|
19
|
+
readonly shell: SettingsFormShell;
|
|
20
|
+
/** One state per control this page owns. */
|
|
21
|
+
readonly fields: Readonly<Record<TeamJudgeField, SettingsFieldState>>;
|
|
22
|
+
}
|
|
23
|
+
export declare class TeamJudgeForm {
|
|
24
|
+
private readonly section;
|
|
25
|
+
private readonly judge;
|
|
26
|
+
private readonly model;
|
|
27
|
+
private readonly scope;
|
|
28
|
+
private readonly store;
|
|
29
|
+
/**
|
|
30
|
+
* @param section - the Team row's configuration form, as the settings provider serves it.
|
|
31
|
+
* @param judge - the status projection a landed write re-reads.
|
|
32
|
+
*/
|
|
33
|
+
constructor(section: SettingsFormScope<TeamJudgeSection>, judge: TeamContextJudgeCheck);
|
|
34
|
+
/** The form state the page renders. */
|
|
35
|
+
readonly getSnapshot: () => TeamJudgeFormSnapshot;
|
|
36
|
+
/** Observe the form state. */
|
|
37
|
+
readonly subscribe: (listener: () => void) => (() => void);
|
|
38
|
+
/** Stage a draft, reset a field, save every staged edit, or discard them. */
|
|
39
|
+
actions(): SettingsFormActions;
|
|
40
|
+
/**
|
|
41
|
+
* Remove the stored key literal: the judge unmounts and the status re-reads.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately its own write rather than a staged edit — the shared form
|
|
44
|
+
* model has no clear for a write-only control, and an empty draft must keep
|
|
45
|
+
* the stored literal instead of clearing it. The page only offers this while
|
|
46
|
+
* no draft is staged, because a write moves the document revision the staged
|
|
47
|
+
* save is fenced to.
|
|
48
|
+
* @returns whether the Host accepted the clear.
|
|
49
|
+
*/
|
|
50
|
+
clearKey(): Promise<boolean>;
|
|
51
|
+
dispose(): void;
|
|
52
|
+
/** Build the page's state from the model's reads and drafts. */
|
|
53
|
+
private project;
|
|
54
|
+
/** Write every staged edit, then re-read the status the Host now serves. */
|
|
55
|
+
save(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/** Read-side face the settings page binds for the endpoint form. */
|
|
58
|
+
export interface TeamJudgeFormSource {
|
|
59
|
+
getSnapshot(): TeamJudgeForm | undefined;
|
|
60
|
+
subscribe(listener: () => void): () => void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The form's seat on the page, as an observable rather than a value.
|
|
64
|
+
*
|
|
65
|
+
* The form exists only while this deployment serves a settings document, and
|
|
66
|
+
* that service may arrive after the page's first render. A slot entry's
|
|
67
|
+
* injected props are computed once and cached for the entry's lifetime, so a
|
|
68
|
+
* value handed over at that moment would freeze the answer: the group would
|
|
69
|
+
* never appear until a reload. The page therefore observes this seat, which
|
|
70
|
+
* the service's arrival and departure both publish through.
|
|
71
|
+
*/
|
|
72
|
+
export declare class TeamJudgeFormSeat implements TeamJudgeFormSource {
|
|
73
|
+
private form;
|
|
74
|
+
private readonly listeners;
|
|
75
|
+
readonly getSnapshot: () => TeamJudgeForm | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Observe the form, without reading anything on subscribe: the seat carries
|
|
78
|
+
* no remote state of its own, so a subscriber renders whatever is held now.
|
|
79
|
+
* @param listener - invoked after every hold or release.
|
|
80
|
+
* @returns the disposer removing this listener.
|
|
81
|
+
*/
|
|
82
|
+
readonly subscribe: (listener: () => void) => (() => void);
|
|
83
|
+
/**
|
|
84
|
+
* Publish the form the settings service answers with.
|
|
85
|
+
* @param form - the form built over that service's section scope.
|
|
86
|
+
*/
|
|
87
|
+
hold(form: TeamJudgeForm): void;
|
|
88
|
+
/**
|
|
89
|
+
* Withdraw the form when its service goes away. Only the holder of the form
|
|
90
|
+
* in place can withdraw it, so a replacement is never cleared by the
|
|
91
|
+
* departure of the one it replaced.
|
|
92
|
+
* @param form - the form this caller holds.
|
|
93
|
+
*/
|
|
94
|
+
release(form: TeamJudgeForm): void;
|
|
95
|
+
dispose(): void;
|
|
96
|
+
private publish;
|
|
97
|
+
}
|
|
98
|
+
export {};
|
|
99
|
+
//# sourceMappingURL=judge-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"judge-form.d.ts","sourceRoot":"","sources":["../../../src/client/judge-form.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,kBAAkB,EAAE,KAAK,mBAAmB,EAA2B,KAAK,iBAAiB,EAClE,KAAK,iBAAiB,EACvD,MAAM,uCAAuC,CAAA;AAC9C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAqB/D,+EAA+E;AAC/E,QAAA,MAAM,WAAW;;;;CAIP,CAAA;AAEV,kCAAkC;AAClC,MAAM,MAAM,cAAc,GAAG,MAAM,OAAO,WAAW,CAAA;AAErD,2EAA2E;AAC3E,MAAM,WAAW,gBAAgB;IAC/B,0CAA0C;IAC1C,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CACvB;AA4DD,4EAA4E;AAC5E,MAAM,WAAW,qBAAqB;IACpC,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAA;IACjC,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC,CAAA;CACtE;AAQD,qBAAa,aAAa;IAUtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAVxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4C;IAClE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IAEtC;;;OAGG;gBAEgB,OAAO,EAAE,iBAAiB,CAAC,gBAAgB,CAAC,EAC5C,KAAK,EAAE,qBAAqB;IAiB/C,uCAAuC;IACvC,QAAQ,CAAC,WAAW,QAAO,qBAAqB,CAA4B;IAE5E,8BAA8B;IAC9B,QAAQ,CAAC,SAAS,GAAI,UAAU,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC,CAAkC;IAE3F,6EAA6E;IAC7E,OAAO,IAAI,mBAAmB;IAI9B;;;;;;;;;OASG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IAYlC,OAAO,IAAI,IAAI;IAIf,gEAAgE;IAChE,OAAO,CAAC,OAAO;IAMf,4EAA4E;IACtE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAI5B;AAED,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IAClC,WAAW,IAAI,aAAa,GAAG,SAAS,CAAA;IACxC,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;CAC5C;AAED;;;;;;;;;GASG;AACH,qBAAa,iBAAkB,YAAW,mBAAmB;IAC3D,OAAO,CAAC,IAAI,CAA2B;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAElD,QAAQ,CAAC,WAAW,QAAO,aAAa,GAAG,SAAS,CAAa;IAEjE;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,GAAI,UAAU,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC,CAGxD;IAED;;;OAGG;IACH,IAAI,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI;IAK/B;;;;;OAKG;IACH,OAAO,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI;IAMlC,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,OAAO;CAGhB"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { SettingsFormModel, settingsTextField, } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
2
|
+
/**
|
|
3
|
+
* The Team settings page's staged form over the judge's endpoint.
|
|
4
|
+
*
|
|
5
|
+
* The page edits one group of the Team row's settings section: the `jev`
|
|
6
|
+
* endpoint the relatedness judge is mounted from. The identity fields that
|
|
7
|
+
* share that section belong to the Human profile rows and are never rendered
|
|
8
|
+
* here, so this form reaches its values through a projection — the shared form
|
|
9
|
+
* model addresses one flat section, and the adapter below maps each control to
|
|
10
|
+
* the path it really lives at.
|
|
11
|
+
*
|
|
12
|
+
* The key literal is the one control that is not read back: the Host redacts
|
|
13
|
+
* it, so its draft starts blank on every load, a blank draft writes nothing,
|
|
14
|
+
* and removing a stored literal is an explicit clear rather than an empty
|
|
15
|
+
* string.
|
|
16
|
+
*/
|
|
17
|
+
/** The group inside the Team row's settings section that this form edits. */
|
|
18
|
+
const JUDGE_GROUP = 'jev';
|
|
19
|
+
/** Where each control's value lives inside the Team row's settings section. */
|
|
20
|
+
const FIELD_PATHS = {
|
|
21
|
+
apiBase: [JUDGE_GROUP, 'apiBase'],
|
|
22
|
+
model: [JUDGE_GROUP, 'model'],
|
|
23
|
+
apiKey: [JUDGE_GROUP, 'apiKey'],
|
|
24
|
+
};
|
|
25
|
+
/** Read the judge group of a section layer as a plain record. */
|
|
26
|
+
function groupOf(layer) {
|
|
27
|
+
if (layer === null || typeof layer !== 'object')
|
|
28
|
+
return undefined;
|
|
29
|
+
const value = layer[JUDGE_GROUP];
|
|
30
|
+
return value !== null && typeof value === 'object' ? value : undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Project one section layer onto the controls this page renders. A control the
|
|
34
|
+
* layer does not carry stays absent rather than becoming an empty string, which
|
|
35
|
+
* is what the model reads to decide whether a field is overridden and what a
|
|
36
|
+
* reset reverts to.
|
|
37
|
+
* @param layer - a section, or one of its composition layers.
|
|
38
|
+
* @returns the flat control values, or undefined when the layer carries none.
|
|
39
|
+
*/
|
|
40
|
+
function flatten(layer) {
|
|
41
|
+
if (layer === null || typeof layer !== 'object')
|
|
42
|
+
return undefined;
|
|
43
|
+
const group = groupOf(layer);
|
|
44
|
+
const flat = {};
|
|
45
|
+
for (const [field, path] of Object.entries(FIELD_PATHS)) {
|
|
46
|
+
if (group !== undefined && Object.hasOwn(group, path[1]))
|
|
47
|
+
flat[field] = group[path[1]];
|
|
48
|
+
}
|
|
49
|
+
return flat;
|
|
50
|
+
}
|
|
51
|
+
/** Address one control's edit at the path it really lives at. */
|
|
52
|
+
function locate(op) {
|
|
53
|
+
const path = FIELD_PATHS[op.path[0]];
|
|
54
|
+
return op.op === 'set'
|
|
55
|
+
? { op: 'set', path: [...path], value: op.value }
|
|
56
|
+
: { op: 'unset', path: [...path] };
|
|
57
|
+
}
|
|
58
|
+
/** The shared form model's view of this page's controls, over the real section. */
|
|
59
|
+
class TeamJudgeScope {
|
|
60
|
+
section;
|
|
61
|
+
constructor(section) {
|
|
62
|
+
this.section = section;
|
|
63
|
+
}
|
|
64
|
+
getSnapshot() {
|
|
65
|
+
const snapshot = this.section.getSnapshot();
|
|
66
|
+
return {
|
|
67
|
+
status: snapshot.status,
|
|
68
|
+
value: flatten(snapshot.value),
|
|
69
|
+
base: flatten(snapshot.base),
|
|
70
|
+
user: flatten(snapshot.user),
|
|
71
|
+
writable: snapshot.writable,
|
|
72
|
+
revision: snapshot.revision,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
subscribe(listener) {
|
|
76
|
+
return this.section.subscribe(listener);
|
|
77
|
+
}
|
|
78
|
+
mutate(ops, expectedRevision) {
|
|
79
|
+
return this.section.mutate(ops.map(op => locate(op)), expectedRevision);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export class TeamJudgeForm {
|
|
83
|
+
section;
|
|
84
|
+
judge;
|
|
85
|
+
model;
|
|
86
|
+
scope;
|
|
87
|
+
store;
|
|
88
|
+
/**
|
|
89
|
+
* @param section - the Team row's configuration form, as the settings provider serves it.
|
|
90
|
+
* @param judge - the status projection a landed write re-reads.
|
|
91
|
+
*/
|
|
92
|
+
constructor(section, judge) {
|
|
93
|
+
this.section = section;
|
|
94
|
+
this.judge = judge;
|
|
95
|
+
this.scope = new TeamJudgeScope(section);
|
|
96
|
+
this.model = new SettingsFormModel(this.scope, [
|
|
97
|
+
settingsTextField('apiBase'),
|
|
98
|
+
settingsTextField('model'),
|
|
99
|
+
], [{
|
|
100
|
+
field: 'apiKey',
|
|
101
|
+
// The literal never rides a response, so a draft is only ever what the
|
|
102
|
+
// reader typed; the write addresses the real path directly.
|
|
103
|
+
write: text => section.mutate([{ op: 'set', path: [...FIELD_PATHS.apiKey], value: text }]),
|
|
104
|
+
}]);
|
|
105
|
+
// The model publishes a projection on every scope change or staged edit;
|
|
106
|
+
// binding one is how this page observes it.
|
|
107
|
+
this.store = this.model.bind(() => this.project());
|
|
108
|
+
}
|
|
109
|
+
/** The form state the page renders. */
|
|
110
|
+
getSnapshot = () => this.store.getSnapshot();
|
|
111
|
+
/** Observe the form state. */
|
|
112
|
+
subscribe = (listener) => this.store.subscribe(listener);
|
|
113
|
+
/** Stage a draft, reset a field, save every staged edit, or discard them. */
|
|
114
|
+
actions() {
|
|
115
|
+
return { ...this.model.actions(), save: () => { void this.save(); } };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Remove the stored key literal: the judge unmounts and the status re-reads.
|
|
119
|
+
*
|
|
120
|
+
* Deliberately its own write rather than a staged edit — the shared form
|
|
121
|
+
* model has no clear for a write-only control, and an empty draft must keep
|
|
122
|
+
* the stored literal instead of clearing it. The page only offers this while
|
|
123
|
+
* no draft is staged, because a write moves the document revision the staged
|
|
124
|
+
* save is fenced to.
|
|
125
|
+
* @returns whether the Host accepted the clear.
|
|
126
|
+
*/
|
|
127
|
+
async clearKey() {
|
|
128
|
+
let landed;
|
|
129
|
+
try {
|
|
130
|
+
landed = await this.section.mutate([{ op: 'unset', path: [...FIELD_PATHS.apiKey] }]);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (!landed)
|
|
136
|
+
return false;
|
|
137
|
+
await this.judge.refresh();
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
dispose() {
|
|
141
|
+
this.model.dispose();
|
|
142
|
+
}
|
|
143
|
+
/** Build the page's state from the model's reads and drafts. */
|
|
144
|
+
project() {
|
|
145
|
+
const fields = {};
|
|
146
|
+
for (const field of Object.keys(FIELD_PATHS))
|
|
147
|
+
fields[field] = this.model.field(field);
|
|
148
|
+
return { shell: this.model.shell(), fields };
|
|
149
|
+
}
|
|
150
|
+
/** Write every staged edit, then re-read the status the Host now serves. */
|
|
151
|
+
async save() {
|
|
152
|
+
await this.model.save();
|
|
153
|
+
if (!this.model.shell().failed)
|
|
154
|
+
await this.judge.refresh();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The form's seat on the page, as an observable rather than a value.
|
|
159
|
+
*
|
|
160
|
+
* The form exists only while this deployment serves a settings document, and
|
|
161
|
+
* that service may arrive after the page's first render. A slot entry's
|
|
162
|
+
* injected props are computed once and cached for the entry's lifetime, so a
|
|
163
|
+
* value handed over at that moment would freeze the answer: the group would
|
|
164
|
+
* never appear until a reload. The page therefore observes this seat, which
|
|
165
|
+
* the service's arrival and departure both publish through.
|
|
166
|
+
*/
|
|
167
|
+
export class TeamJudgeFormSeat {
|
|
168
|
+
form;
|
|
169
|
+
listeners = new Set();
|
|
170
|
+
getSnapshot = () => this.form;
|
|
171
|
+
/**
|
|
172
|
+
* Observe the form, without reading anything on subscribe: the seat carries
|
|
173
|
+
* no remote state of its own, so a subscriber renders whatever is held now.
|
|
174
|
+
* @param listener - invoked after every hold or release.
|
|
175
|
+
* @returns the disposer removing this listener.
|
|
176
|
+
*/
|
|
177
|
+
subscribe = (listener) => {
|
|
178
|
+
this.listeners.add(listener);
|
|
179
|
+
return () => { this.listeners.delete(listener); };
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Publish the form the settings service answers with.
|
|
183
|
+
* @param form - the form built over that service's section scope.
|
|
184
|
+
*/
|
|
185
|
+
hold(form) {
|
|
186
|
+
this.form = form;
|
|
187
|
+
this.publish();
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Withdraw the form when its service goes away. Only the holder of the form
|
|
191
|
+
* in place can withdraw it, so a replacement is never cleared by the
|
|
192
|
+
* departure of the one it replaced.
|
|
193
|
+
* @param form - the form this caller holds.
|
|
194
|
+
*/
|
|
195
|
+
release(form) {
|
|
196
|
+
if (this.form !== form)
|
|
197
|
+
return;
|
|
198
|
+
this.form = undefined;
|
|
199
|
+
this.publish();
|
|
200
|
+
}
|
|
201
|
+
dispose() {
|
|
202
|
+
this.listeners.clear();
|
|
203
|
+
}
|
|
204
|
+
publish() {
|
|
205
|
+
for (const listener of this.listeners)
|
|
206
|
+
listener();
|
|
207
|
+
}
|
|
208
|
+
}
|