@stigmer/react 3.1.7 → 3.1.9
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/agent/AgentDetailView.d.ts +9 -1
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +15 -4
- package/agent/AgentDetailView.js.map +1 -1
- package/environment/EnvironmentListPanel.d.ts.map +1 -1
- package/environment/EnvironmentListPanel.js +11 -3
- package/environment/EnvironmentListPanel.js.map +1 -1
- package/environment/EnvironmentPicker.d.ts +17 -1
- package/environment/EnvironmentPicker.d.ts.map +1 -1
- package/environment/EnvironmentPicker.js +3 -2
- package/environment/EnvironmentPicker.js.map +1 -1
- package/environment/EnvironmentVariableEditor.d.ts.map +1 -1
- package/environment/EnvironmentVariableEditor.js +9 -3
- package/environment/EnvironmentVariableEditor.js.map +1 -1
- package/environment/index.d.ts +1 -0
- package/environment/index.d.ts.map +1 -1
- package/environment/index.js +1 -0
- package/environment/index.js.map +1 -1
- package/environment/shareRestriction.d.ts +13 -0
- package/environment/shareRestriction.d.ts.map +1 -0
- package/environment/shareRestriction.js +15 -0
- package/environment/shareRestriction.js.map +1 -0
- package/execution/ToolCallItem.d.ts.map +1 -1
- package/execution/ToolCallItem.js +12 -0
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/ToolRunGroup.js +4 -0
- package/execution/ToolRunGroup.js.map +1 -1
- package/execution/WriteBackCard.d.ts +14 -5
- package/execution/WriteBackCard.d.ts.map +1 -1
- package/execution/WriteBackCard.js +103 -32
- package/execution/WriteBackCard.js.map +1 -1
- package/execution/WriteBacksWidget.d.ts +1 -1
- package/execution/WriteBacksWidget.js +2 -2
- package/execution/WriteBacksWidget.js.map +1 -1
- package/execution/index.d.ts +2 -0
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +1 -0
- package/execution/index.js.map +1 -1
- package/execution/write-back-utils.d.ts +57 -0
- package/execution/write-back-utils.d.ts.map +1 -0
- package/execution/write-back-utils.js +81 -0
- package/execution/write-back-utils.js.map +1 -0
- package/index.d.ts +6 -2
- package/index.d.ts.map +1 -1
- package/index.js +6 -1
- package/index.js.map +1 -1
- package/internal/dev/dom-counter.d.ts.map +1 -1
- package/internal/dev/dom-counter.js +2 -2
- package/internal/dev/dom-counter.js.map +1 -1
- package/internal/dev/enabled.d.ts +7 -0
- package/internal/dev/enabled.d.ts.map +1 -0
- package/internal/dev/enabled.js +38 -0
- package/internal/dev/enabled.js.map +1 -0
- package/internal/dev/profiler-wrapper.d.ts +3 -3
- package/internal/dev/profiler-wrapper.d.ts.map +1 -1
- package/internal/dev/profiler-wrapper.js +5 -5
- package/internal/dev/profiler-wrapper.js.map +1 -1
- package/internal/dev/use-key-stability.d.ts.map +1 -1
- package/internal/dev/use-key-stability.js +2 -2
- package/internal/dev/use-key-stability.js.map +1 -1
- package/internal/dev/use-render-tracer.d.ts +2 -2
- package/internal/dev/use-render-tracer.d.ts.map +1 -1
- package/internal/dev/use-render-tracer.js +4 -4
- package/internal/dev/use-render-tracer.js.map +1 -1
- package/internal/dev/use-stream-rate.d.ts +2 -1
- package/internal/dev/use-stream-rate.d.ts.map +1 -1
- package/internal/dev/use-stream-rate.js +4 -3
- package/internal/dev/use-stream-rate.js.map +1 -1
- package/library/ResourceVisibilityControl.d.ts +3 -0
- package/library/ResourceVisibilityControl.d.ts.map +1 -1
- package/library/ResourceVisibilityControl.js +22 -8
- package/library/ResourceVisibilityControl.js.map +1 -1
- package/library/useUpdateVisibility.d.ts +5 -3
- package/library/useUpdateVisibility.d.ts.map +1 -1
- package/library/useUpdateVisibility.js +7 -2
- package/library/useUpdateVisibility.js.map +1 -1
- package/library/visibilityLevels.d.ts +17 -0
- package/library/visibilityLevels.d.ts.map +1 -1
- package/library/visibilityLevels.js +32 -0
- package/library/visibilityLevels.js.map +1 -1
- package/package.json +4 -4
- package/session/NewSessionViewer.d.ts +11 -1
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +28 -8
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +9 -2
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +26 -12
- package/session/SessionViewer.js.map +1 -1
- package/session/audience.d.ts +12 -4
- package/session/audience.d.ts.map +1 -1
- package/session/facets/ChangesTab.d.ts.map +1 -1
- package/session/facets/ChangesTab.js +3 -1
- package/session/facets/ChangesTab.js.map +1 -1
- package/session/useNewSessionFlow.d.ts +12 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +13 -1
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +13 -0
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +7 -2
- package/session/useSessionPageFlow.js.map +1 -1
- package/sharing/ShareAgentDialog.d.ts +57 -0
- package/sharing/ShareAgentDialog.d.ts.map +1 -0
- package/sharing/ShareAgentDialog.js +478 -0
- package/sharing/ShareAgentDialog.js.map +1 -0
- package/sharing/SharedAgentChat.d.ts +84 -0
- package/sharing/SharedAgentChat.d.ts.map +1 -0
- package/sharing/SharedAgentChat.js +80 -0
- package/sharing/SharedAgentChat.js.map +1 -0
- package/sharing/index.d.ts +18 -0
- package/sharing/index.d.ts.map +1 -0
- package/sharing/index.js +12 -0
- package/sharing/index.js.map +1 -0
- package/sharing/useAgentShare.d.ts +43 -0
- package/sharing/useAgentShare.d.ts.map +1 -0
- package/sharing/useAgentShare.js +54 -0
- package/sharing/useAgentShare.js.map +1 -0
- package/sharing/useRotateShareLink.d.ts +45 -0
- package/sharing/useRotateShareLink.d.ts.map +1 -0
- package/sharing/useRotateShareLink.js +58 -0
- package/sharing/useRotateShareLink.js.map +1 -0
- package/sharing/useSaveAgentShare.d.ts +105 -0
- package/sharing/useSaveAgentShare.d.ts.map +1 -0
- package/sharing/useSaveAgentShare.js +97 -0
- package/sharing/useSaveAgentShare.js.map +1 -0
- package/sharing/useShareAgent.d.ts +69 -0
- package/sharing/useShareAgent.d.ts.map +1 -0
- package/sharing/useShareAgent.js +42 -0
- package/sharing/useShareAgent.js.map +1 -0
- package/sharing/useShareToolReadiness.d.ts +52 -0
- package/sharing/useShareToolReadiness.d.ts.map +1 -0
- package/sharing/useShareToolReadiness.js +89 -0
- package/sharing/useShareToolReadiness.js.map +1 -0
- package/sharing/useSharedAgentProfile.d.ts +85 -0
- package/sharing/useSharedAgentProfile.d.ts.map +1 -0
- package/sharing/useSharedAgentProfile.js +73 -0
- package/sharing/useSharedAgentProfile.js.map +1 -0
- package/src/agent/AgentDetailView.tsx +28 -3
- package/src/environment/EnvironmentListPanel.tsx +47 -18
- package/src/environment/EnvironmentPicker.tsx +23 -2
- package/src/environment/EnvironmentVariableEditor.tsx +27 -17
- package/src/environment/__tests__/shareRestriction.test.ts +35 -0
- package/src/environment/index.ts +5 -0
- package/src/environment/shareRestriction.ts +22 -0
- package/src/execution/ToolCallItem.tsx +19 -1
- package/src/execution/ToolRunGroup.tsx +4 -0
- package/src/execution/WriteBackCard.tsx +285 -88
- package/src/execution/WriteBacksWidget.tsx +5 -5
- package/src/execution/__tests__/ToolCallItem.test.tsx +51 -0
- package/src/execution/__tests__/ToolRunGroup.test.tsx +22 -0
- package/src/execution/__tests__/WriteBackCard.test.tsx +64 -10
- package/src/execution/__tests__/write-back-utils.test.ts +135 -0
- package/src/execution/index.ts +7 -0
- package/src/execution/write-back-utils.ts +107 -0
- package/src/index.ts +38 -0
- package/src/internal/dev/__tests__/use-key-stability.test.ts +4 -0
- package/src/internal/dev/__tests__/use-render-tracer.test.ts +4 -0
- package/src/internal/dev/dom-counter.ts +2 -3
- package/src/internal/dev/enabled.ts +39 -0
- package/src/internal/dev/profiler-wrapper.tsx +5 -6
- package/src/internal/dev/use-key-stability.ts +2 -3
- package/src/internal/dev/use-render-tracer.ts +4 -5
- package/src/internal/dev/use-stream-rate.ts +4 -4
- package/src/library/ResourceVisibilityControl.tsx +23 -7
- package/src/library/__tests__/visibilityLevels.test.ts +33 -0
- package/src/library/useUpdateVisibility.ts +9 -3
- package/src/library/visibilityLevels.ts +36 -0
- package/src/session/NewSessionViewer.tsx +67 -31
- package/src/session/SessionViewer.tsx +67 -36
- package/src/session/__tests__/audienceWiring.test.tsx +99 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +75 -1
- package/src/session/__tests__/useSessionPageFlow.guest.test.tsx +112 -0
- package/src/session/audience.ts +12 -4
- package/src/session/facets/ChangesTab.tsx +6 -4
- package/src/session/useNewSessionFlow.ts +26 -1
- package/src/session/useSessionPageFlow.ts +24 -2
- package/src/sharing/ShareAgentDialog.tsx +1489 -0
- package/src/sharing/SharedAgentChat.tsx +283 -0
- package/src/sharing/__tests__/ShareAgentDialog.test.tsx +926 -0
- package/src/sharing/__tests__/SharedAgentChat.test.tsx +222 -0
- package/src/sharing/__tests__/useAgentShare.test.tsx +149 -0
- package/src/sharing/__tests__/useRotateShareLink.test.tsx +96 -0
- package/src/sharing/__tests__/useSaveAgentShare.test.tsx +241 -0
- package/src/sharing/__tests__/useShareAgent.test.tsx +131 -0
- package/src/sharing/__tests__/useShareToolReadiness.test.tsx +219 -0
- package/src/sharing/__tests__/useSharedAgentProfile.test.tsx +201 -0
- package/src/sharing/index.ts +32 -0
- package/src/sharing/useAgentShare.ts +91 -0
- package/src/sharing/useRotateShareLink.ts +81 -0
- package/src/sharing/useSaveAgentShare.ts +186 -0
- package/src/sharing/useShareAgent.tsx +107 -0
- package/src/sharing/useShareToolReadiness.ts +131 -0
- package/src/sharing/useSharedAgentProfile.ts +132 -0
- package/src/switch/Switch.tsx +97 -0
- package/src/switch/__tests__/Switch.test.tsx +107 -0
- package/src/switch/index.ts +2 -0
- package/styles.css +1 -1
- package/switch/Switch.d.ts +47 -0
- package/switch/Switch.d.ts.map +1 -0
- package/switch/Switch.js +41 -0
- package/switch/Switch.js.map +1 -0
- package/switch/index.d.ts +3 -0
- package/switch/index.d.ts.map +1 -0
- package/switch/index.js +2 -0
- package/switch/index.js.map +1 -0
|
@@ -143,6 +143,42 @@ export const INSTANCE_VISIBILITY_LEVELS: readonly VisibilityLevelOption[] = [
|
|
|
143
143
|
},
|
|
144
144
|
];
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The levels an environment selector offers, in escalation order:
|
|
148
|
+
* Private / Organization. Broader levels are structurally absent —
|
|
149
|
+
* secret values never leave the org boundary (the backend rejects
|
|
150
|
+
* public/platform via the kind's VisibilityConfig).
|
|
151
|
+
*
|
|
152
|
+
* Org sharing on an environment carries credential semantics, so the
|
|
153
|
+
* copy names both effects: members get redacted view, and executions
|
|
154
|
+
* in the org (teammate-run agents AND shared-agent visitors) can use
|
|
155
|
+
* the values at runtime. Secret reveal stays creator-only at every
|
|
156
|
+
* level.
|
|
157
|
+
*
|
|
158
|
+
* In `local` mode (OSS Go backend, single-user) sharing has no
|
|
159
|
+
* enforcement meaning, so no interactive levels are offered —
|
|
160
|
+
* {@link ResourceVisibilityControl} degrades to a read-only badge.
|
|
161
|
+
*/
|
|
162
|
+
export function environmentVisibilityLevels(
|
|
163
|
+
deploymentMode: "cloud" | "local",
|
|
164
|
+
): readonly VisibilityLevelOption[] {
|
|
165
|
+
if (deploymentMode === "local") {
|
|
166
|
+
return [PRIVATE_OPTION];
|
|
167
|
+
}
|
|
168
|
+
return [
|
|
169
|
+
{
|
|
170
|
+
...PRIVATE_OPTION,
|
|
171
|
+
description: "Only you can view and use these credentials",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
...ORG_OPTION,
|
|
175
|
+
description:
|
|
176
|
+
"Agents run in your org — including shared-agent visitors — can use these credentials. Members see names only; secret values stay hidden.",
|
|
177
|
+
confirmPrompt: "Let agents run in your org use these credentials?",
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
|
|
146
182
|
/**
|
|
147
183
|
* Canonical option for a visibility value, independent of any kind's offered
|
|
148
184
|
* list. Used to render the current level even when it is not offerable in
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback, useMemo, useState, type ReactNode } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
5
|
import type { ResourceRef } from "@stigmer/sdk";
|
|
6
6
|
import type { UseGitHubConnectionReturn } from "../github/useGitHubConnection.js";
|
|
@@ -83,7 +83,17 @@ export interface NewSessionViewerProps {
|
|
|
83
83
|
* server, skill, and session-variable pickers — for product-embedded
|
|
84
84
|
* chat where the agent is configured upstream by the platform. The
|
|
85
85
|
* model selector, interaction mode, harness selector, attachments,
|
|
86
|
-
* and workspace picker remain.
|
|
86
|
+
* and workspace picker remain.
|
|
87
|
+
*
|
|
88
|
+
* `"guest"` (anonymous visitor with a guest token) is pure chat: it
|
|
89
|
+
* additionally hides the model/harness/mode pickers, attachments,
|
|
90
|
+
* the workspace picker, and the session panel, binds the session to
|
|
91
|
+
* `initialAgentRef` + `initialInstanceId` without any picker
|
|
92
|
+
* machinery, and skips the org-level reads a guest principal cannot
|
|
93
|
+
* make. Requires both `initialAgentRef` and `initialInstanceId` —
|
|
94
|
+
* a guest launcher never falls back to the org default agent.
|
|
95
|
+
*
|
|
96
|
+
* See {@link SessionAudience}.
|
|
87
97
|
*
|
|
88
98
|
* @default "integrator"
|
|
89
99
|
*/
|
|
@@ -198,9 +208,26 @@ export function NewSessionViewer({
|
|
|
198
208
|
onError,
|
|
199
209
|
getRuntimeEnv,
|
|
200
210
|
defaultHarness,
|
|
211
|
+
audience,
|
|
201
212
|
});
|
|
202
213
|
const [interactionMode, setInteractionMode] = useState<InteractionModeOption>("agent");
|
|
203
|
-
const
|
|
214
|
+
const isGuest = audience === "guest";
|
|
215
|
+
// Both curated audiences (endUser, guest) lock the agent and hide the
|
|
216
|
+
// integrator pickers; guest adds its own restrictions below.
|
|
217
|
+
const isCurated = audience !== "integrator";
|
|
218
|
+
|
|
219
|
+
// Guest agent binding is host configuration, not a picker interaction:
|
|
220
|
+
// the composer's agent machinery (picker, env-collection, personal
|
|
221
|
+
// environments — all org reads a guest token cannot make) stays fully
|
|
222
|
+
// unwired, and the launcher pins the flow to the shared agent's
|
|
223
|
+
// instance directly. Without the pin, submission fails closed in
|
|
224
|
+
// useNewSessionFlow rather than falling back to the org default agent.
|
|
225
|
+
const { setAgentRef, setResolution } = flow;
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (!isGuest || !initialAgentRef || !initialInstanceId) return;
|
|
228
|
+
setAgentRef(initialAgentRef);
|
|
229
|
+
setResolution({ mode: "saved", instanceId: initialInstanceId });
|
|
230
|
+
}, [isGuest, initialAgentRef, initialInstanceId, setAgentRef, setResolution]);
|
|
204
231
|
|
|
205
232
|
// The unified-panel controller (shared with SessionViewer, DD-016). The
|
|
206
233
|
// launcher has no execution yet, so the FSM inputs are static. It has no
|
|
@@ -259,9 +286,9 @@ export function NewSessionViewer({
|
|
|
259
286
|
harness: flow.harness,
|
|
260
287
|
executionTarget: undefined,
|
|
261
288
|
modelId: flow.modelId,
|
|
262
|
-
//
|
|
263
|
-
// tab renders read-only without mutation callbacks (DD-011).
|
|
264
|
-
mutations:
|
|
289
|
+
// Curated audiences see the configuration but cannot strip it — the
|
|
290
|
+
// Setup tab renders read-only without mutation callbacks (DD-011).
|
|
291
|
+
mutations: isCurated
|
|
265
292
|
? undefined
|
|
266
293
|
: {
|
|
267
294
|
onRemoveAgent: flow.agentRef ? handleRemoveAgent : undefined,
|
|
@@ -272,7 +299,7 @@ export function NewSessionViewer({
|
|
|
272
299
|
[
|
|
273
300
|
flow.agentRef, flow.mcpServerUsages, flow.skillRefs,
|
|
274
301
|
flow.sessionVariables, flow.harness, flow.modelId,
|
|
275
|
-
|
|
302
|
+
isCurated, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
|
|
276
303
|
],
|
|
277
304
|
);
|
|
278
305
|
|
|
@@ -312,29 +339,33 @@ export function NewSessionViewer({
|
|
|
312
339
|
onSubmit={flow.submit}
|
|
313
340
|
isSubmitting={flow.isSubmitting}
|
|
314
341
|
org={org}
|
|
315
|
-
workspace={flow.workspace}
|
|
316
|
-
gitHubConnection={enableGitHub ? gitHubConnection : undefined}
|
|
317
|
-
enableGitHub={enableGitHub}
|
|
318
|
-
enableLocal={enableLocal}
|
|
342
|
+
workspace={isGuest ? undefined : flow.workspace}
|
|
343
|
+
gitHubConnection={enableGitHub && !isGuest ? gitHubConnection : undefined}
|
|
344
|
+
enableGitHub={enableGitHub && !isGuest}
|
|
345
|
+
enableLocal={enableLocal && !isGuest}
|
|
319
346
|
onBrowseLocalFolder={onBrowseLocalFolder}
|
|
320
347
|
agentRef={flow.agentRef}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
348
|
+
// Guests get no agent machinery at all — the binding is applied
|
|
349
|
+
// by the launcher's pin effect above, not by picker resolution.
|
|
350
|
+
onAgentRefChange={isGuest ? undefined : flow.setAgentRef}
|
|
351
|
+
onAgentResolutionChange={isGuest ? undefined : flow.setResolution}
|
|
352
|
+
initialAgentRef={isGuest ? undefined : initialAgentRef}
|
|
353
|
+
initialInstanceId={isGuest ? undefined : initialInstanceId}
|
|
354
|
+
initialAttachments={isGuest ? undefined : initialAttachments}
|
|
355
|
+
lockAgent={isCurated && initialAgentRef != null}
|
|
356
|
+
mcpServerUsages={isCurated ? undefined : flow.mcpServerUsages}
|
|
357
|
+
onMcpServerUsagesChange={isCurated ? undefined : flow.setMcpServerUsages}
|
|
358
|
+
skillRefs={isCurated ? undefined : flow.skillRefs}
|
|
359
|
+
onSkillRefsChange={isCurated ? undefined : flow.setSkillRefs}
|
|
360
|
+
sessionVariables={isCurated ? undefined : flow.sessionVariables}
|
|
361
|
+
showHarnessSelector={!isGuest}
|
|
333
362
|
harness={flow.harness}
|
|
334
363
|
onHarnessChange={flow.setHarness}
|
|
335
364
|
interactionMode={interactionMode}
|
|
336
365
|
onInteractionModeChange={setInteractionMode}
|
|
337
|
-
showInteractionModePicker
|
|
366
|
+
showInteractionModePicker={!isGuest}
|
|
367
|
+
showModelSelector={!isGuest}
|
|
368
|
+
enableAttachments={!isGuest}
|
|
338
369
|
defaultModelId={flow.modelId}
|
|
339
370
|
onModelChange={flow.setModelId}
|
|
340
371
|
placeholder={placeholder}
|
|
@@ -360,13 +391,18 @@ export function NewSessionViewer({
|
|
|
360
391
|
reads as instability and, worse, unmounts the open panel's only
|
|
361
392
|
collapse control when the last context item is removed. Always-on is
|
|
362
393
|
the predictable, discoverable shape; opening homes on the Config
|
|
363
|
-
facet, which carries useful defaults (harness/model) pre-session.
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
394
|
+
facet, which carries useful defaults (harness/model) pre-session.
|
|
395
|
+
Guests are the exception: the panel exposes configuration a visitor
|
|
396
|
+
has no business with, so the chip (the panel's only toggle) is
|
|
397
|
+
simply absent and the panel can never open. */}
|
|
398
|
+
{!isGuest && (
|
|
399
|
+
<div className="absolute top-2 right-6 z-10">
|
|
400
|
+
<SessionPanelChip
|
|
401
|
+
isOpen={panel.isOpen}
|
|
402
|
+
onToggle={panel.isOpen ? panel.closePanel : panel.openPanel}
|
|
403
|
+
/>
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
370
406
|
|
|
371
407
|
{/* Same unified-panel layout as SessionViewer (DD-016): collapsed by
|
|
372
408
|
default (composer fills the row); opening makes the composer the
|
|
@@ -209,8 +209,15 @@ export interface SessionViewerProps {
|
|
|
209
209
|
* session-variable configuration in both the composer and the
|
|
210
210
|
* inspector's Setup tab — for product-embedded chat where the agent
|
|
211
211
|
* is configured upstream by the platform. The model selector,
|
|
212
|
-
* interaction mode, attachments, and workspace picker remain.
|
|
213
|
-
*
|
|
212
|
+
* interaction mode, attachments, and workspace picker remain.
|
|
213
|
+
*
|
|
214
|
+
* `"guest"` (anonymous visitor with a guest token) is pure chat: it
|
|
215
|
+
* additionally hides the model/mode pickers, attachments, the
|
|
216
|
+
* workspace picker, and the session panel, and skips the org-level
|
|
217
|
+
* reads a guest principal cannot make — follow-ups simply continue
|
|
218
|
+
* on the session's bound agent instance.
|
|
219
|
+
*
|
|
220
|
+
* See {@link SessionAudience}.
|
|
214
221
|
*
|
|
215
222
|
* @default "integrator"
|
|
216
223
|
*/
|
|
@@ -303,9 +310,12 @@ export function SessionViewer({
|
|
|
303
310
|
threadSlots,
|
|
304
311
|
className,
|
|
305
312
|
}: SessionViewerProps) {
|
|
306
|
-
const flow = useSessionPageFlow({ sessionId, org, getRuntimeEnv });
|
|
313
|
+
const flow = useSessionPageFlow({ sessionId, org, getRuntimeEnv, audience });
|
|
307
314
|
const { conv } = flow;
|
|
308
|
-
const
|
|
315
|
+
const isGuest = audience === "guest";
|
|
316
|
+
// Both curated audiences (endUser, guest) lock the agent and hide the
|
|
317
|
+
// integrator configuration; guest adds its own restrictions below.
|
|
318
|
+
const isCurated = audience !== "integrator";
|
|
309
319
|
|
|
310
320
|
const [modelId, setModelId] = flow.model;
|
|
311
321
|
const [interactionMode, setInteractionMode] = flow.interactionMode;
|
|
@@ -391,7 +401,9 @@ export function SessionViewer({
|
|
|
391
401
|
const panel = useSessionPanel({
|
|
392
402
|
phase: hasPhase ? phase : null,
|
|
393
403
|
hasChanges: hasWriteBacks,
|
|
394
|
-
|
|
404
|
+
// A streaming plan auto-opens the panel via its key — but guests have
|
|
405
|
+
// no panel, so the trigger is suppressed along with the chip.
|
|
406
|
+
planKey: isGuest ? null : panelPlanKey,
|
|
395
407
|
});
|
|
396
408
|
|
|
397
409
|
const handleBuildFromPlan = useCallback(() => {
|
|
@@ -504,6 +516,10 @@ export function SessionViewer({
|
|
|
504
516
|
// (which subscribes), never this streaming column.
|
|
505
517
|
const handleTranscriptFilePathClick = useCallback(
|
|
506
518
|
(path: string): boolean => {
|
|
519
|
+
// Guests have no panel (and no file viewer), so paths keep their
|
|
520
|
+
// default copy behavior instead of opening a surface that cannot
|
|
521
|
+
// render.
|
|
522
|
+
if (isGuest) return false;
|
|
507
523
|
const selection = resolveWorkspaceFileSelection(
|
|
508
524
|
path,
|
|
509
525
|
flow.workspace.entries,
|
|
@@ -513,7 +529,7 @@ export function SessionViewer({
|
|
|
513
529
|
panel.openFile(selection.entryId, selection.path);
|
|
514
530
|
return true;
|
|
515
531
|
},
|
|
516
|
-
[flow.workspace.entries, flow.sandboxWorkspaceRoot, panel.openFile],
|
|
532
|
+
[isGuest, flow.workspace.entries, flow.sandboxWorkspaceRoot, panel.openFile],
|
|
517
533
|
);
|
|
518
534
|
|
|
519
535
|
if (conv.isLoading) {
|
|
@@ -537,14 +553,18 @@ export function SessionViewer({
|
|
|
537
553
|
{/* Top-right controls: host actions + the panel chip. The chip is the
|
|
538
554
|
panel's always-mounted toggle; while collapsed it carries only the
|
|
539
555
|
pending-item count. Execution status is never surfaced as header
|
|
540
|
-
chrome — the thread itself communicates run state.
|
|
556
|
+
chrome — the thread itself communicates run state. Guests get no
|
|
557
|
+
chip: the panel exposes session configuration a visitor has no
|
|
558
|
+
business with, so its only toggle is simply absent. */}
|
|
541
559
|
<div className="absolute top-2 right-6 z-10 flex items-center gap-2">
|
|
542
560
|
{headerActions}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
561
|
+
{!isGuest && (
|
|
562
|
+
<SessionPanelChip
|
|
563
|
+
isOpen={panel.isOpen}
|
|
564
|
+
onToggle={panel.isOpen ? panel.closePanel : panel.openPanel}
|
|
565
|
+
badgeCount={writeBackCount + artifactCount}
|
|
566
|
+
/>
|
|
567
|
+
)}
|
|
548
568
|
</div>
|
|
549
569
|
|
|
550
570
|
<ThreadSelectionContext.Provider value={selectionStore}>
|
|
@@ -583,12 +603,13 @@ export function SessionViewer({
|
|
|
583
603
|
planAttachFailed={planAttachFailed}
|
|
584
604
|
onDismissPlanAttachFailed={() => setPlanAttachFailed(false)}
|
|
585
605
|
onFilePathClick={handleTranscriptFilePathClick}
|
|
586
|
-
|
|
606
|
+
isCurated={isCurated}
|
|
607
|
+
isGuest={isGuest}
|
|
587
608
|
threadSlots={threadSlots}
|
|
588
609
|
/>
|
|
589
610
|
}
|
|
590
611
|
secondary={
|
|
591
|
-
panel.isOpen ? (
|
|
612
|
+
panel.isOpen && !isGuest ? (
|
|
592
613
|
<SessionPanelRegion
|
|
593
614
|
flow={flow}
|
|
594
615
|
org={org}
|
|
@@ -609,7 +630,7 @@ export function SessionViewer({
|
|
|
609
630
|
workspaceFileLister={workspaceFileLister}
|
|
610
631
|
workspaceFileReader={workspaceFileReader}
|
|
611
632
|
workspaceContentSearcher={workspaceContentSearcher}
|
|
612
|
-
|
|
633
|
+
isCurated={isCurated}
|
|
613
634
|
/>
|
|
614
635
|
) : null
|
|
615
636
|
}
|
|
@@ -649,7 +670,10 @@ interface ConversationColumnProps {
|
|
|
649
670
|
* let the path keep its copy / GitHub-link behavior.
|
|
650
671
|
*/
|
|
651
672
|
readonly onFilePathClick: (path: string) => boolean;
|
|
652
|
-
|
|
673
|
+
/** Curated audience (endUser or guest): locked agent, no integrator pickers. */
|
|
674
|
+
readonly isCurated: boolean;
|
|
675
|
+
/** Guest audience: pure chat — additionally no model picker, mode picker, attachments, or workspace. */
|
|
676
|
+
readonly isGuest: boolean;
|
|
653
677
|
readonly threadSlots?: MessageThreadSlots;
|
|
654
678
|
}
|
|
655
679
|
|
|
@@ -671,7 +695,8 @@ const ConversationColumn = memo(function ConversationColumn({
|
|
|
671
695
|
planAttachFailed,
|
|
672
696
|
onDismissPlanAttachFailed,
|
|
673
697
|
onFilePathClick,
|
|
674
|
-
|
|
698
|
+
isCurated,
|
|
699
|
+
isGuest,
|
|
675
700
|
threadSlots,
|
|
676
701
|
}: ConversationColumnProps) {
|
|
677
702
|
const { conv } = flow;
|
|
@@ -839,22 +864,27 @@ const ConversationColumn = memo(function ConversationColumn({
|
|
|
839
864
|
onModelChange={setModelId}
|
|
840
865
|
interactionMode={interactionMode}
|
|
841
866
|
onInteractionModeChange={setInteractionMode}
|
|
842
|
-
showInteractionModePicker
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
867
|
+
showInteractionModePicker={!isGuest}
|
|
868
|
+
showModelSelector={!isGuest}
|
|
869
|
+
enableAttachments={!isGuest}
|
|
870
|
+
workspace={isGuest ? undefined : flow.workspace}
|
|
871
|
+
gitHubConnection={isGuest ? undefined : gitHubConnection}
|
|
872
|
+
enableGitHub={enableGitHub && !isGuest}
|
|
873
|
+
enableLocal={enableLocal && !isGuest}
|
|
847
874
|
onBrowseLocalFolder={onBrowseLocalFolder}
|
|
848
875
|
agentRef={flow.agentRef}
|
|
849
|
-
|
|
850
|
-
|
|
876
|
+
// Guests get no agent machinery: the session's agent is already
|
|
877
|
+
// bound server-side, and the picker's resolution path performs
|
|
878
|
+
// org reads a guest token cannot make.
|
|
879
|
+
onAgentRefChange={isGuest ? undefined : flow.setAgentRef}
|
|
880
|
+
onAgentResolutionChange={isGuest ? undefined : flow.setResolution}
|
|
851
881
|
isDefaultAgent={flow.isDefaultAgent}
|
|
852
|
-
lockAgent={
|
|
853
|
-
mcpServerUsages={
|
|
854
|
-
onMcpServerUsagesChange={
|
|
855
|
-
skillRefs={
|
|
856
|
-
onSkillRefsChange={
|
|
857
|
-
sessionVariables={
|
|
882
|
+
lockAgent={isCurated}
|
|
883
|
+
mcpServerUsages={isCurated ? undefined : flow.mcpServerUsages}
|
|
884
|
+
onMcpServerUsagesChange={isCurated ? undefined : flow.setMcpServerUsages}
|
|
885
|
+
skillRefs={isCurated ? undefined : flow.skillRefs}
|
|
886
|
+
onSkillRefsChange={isCurated ? undefined : flow.setSkillRefs}
|
|
887
|
+
sessionVariables={isCurated ? undefined : flow.sessionVariables}
|
|
858
888
|
className="px-4 py-3"
|
|
859
889
|
/>
|
|
860
890
|
</div>
|
|
@@ -909,7 +939,8 @@ interface SessionPanelRegionProps {
|
|
|
909
939
|
readonly workspaceFileLister?: WorkspaceFileLister;
|
|
910
940
|
readonly workspaceFileReader?: WorkspaceFileReader;
|
|
911
941
|
readonly workspaceContentSearcher?: WorkspaceContentSearcher;
|
|
912
|
-
|
|
942
|
+
/** Curated audience (endUser or guest): the Setup facet renders read-only. */
|
|
943
|
+
readonly isCurated: boolean;
|
|
913
944
|
}
|
|
914
945
|
|
|
915
946
|
function SessionPanelRegion({
|
|
@@ -932,7 +963,7 @@ function SessionPanelRegion({
|
|
|
932
963
|
workspaceFileLister,
|
|
933
964
|
workspaceFileReader,
|
|
934
965
|
workspaceContentSearcher,
|
|
935
|
-
|
|
966
|
+
isCurated,
|
|
936
967
|
}: SessionPanelRegionProps) {
|
|
937
968
|
const selectedItem = useSelectedThreadItem();
|
|
938
969
|
const { editors, activeKey, activeFile, reveal } = useWorkspaceEditors(
|
|
@@ -1108,9 +1139,9 @@ function SessionPanelRegion({
|
|
|
1108
1139
|
harness: flow.harness,
|
|
1109
1140
|
executionTarget: flow.executionTarget,
|
|
1110
1141
|
modelId: flow.model[0],
|
|
1111
|
-
//
|
|
1112
|
-
// facet renders read-only without mutation callbacks (DD-011).
|
|
1113
|
-
mutations:
|
|
1142
|
+
// Curated audiences see the configuration but cannot strip it — the
|
|
1143
|
+
// Config facet renders read-only without mutation callbacks (DD-011).
|
|
1144
|
+
mutations: isCurated
|
|
1114
1145
|
? undefined
|
|
1115
1146
|
: {
|
|
1116
1147
|
onRemoveAgent: flow.isDefaultAgent ? undefined : handleRemoveAgent,
|
|
@@ -1122,7 +1153,7 @@ function SessionPanelRegion({
|
|
|
1122
1153
|
[
|
|
1123
1154
|
flow.agentRef, flow.isDefaultAgent, flow.mcpServerUsages, flow.skillRefs,
|
|
1124
1155
|
flow.sessionVariables, flow.harness, flow.executionTarget, flow.model,
|
|
1125
|
-
|
|
1156
|
+
isCurated, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
|
|
1126
1157
|
accessSlot,
|
|
1127
1158
|
],
|
|
1128
1159
|
);
|
|
@@ -249,6 +249,75 @@ describe("NewSessionViewer — audience wiring", () => {
|
|
|
249
249
|
expect.objectContaining({ getRuntimeEnv, defaultHarness: "cursor" }),
|
|
250
250
|
);
|
|
251
251
|
});
|
|
252
|
+
|
|
253
|
+
it("guest: pure chat — every picker hidden, attachments and workspace off", () => {
|
|
254
|
+
render(
|
|
255
|
+
<NewSessionViewer
|
|
256
|
+
org="acme"
|
|
257
|
+
onSessionCreated={vi.fn()}
|
|
258
|
+
audience="guest"
|
|
259
|
+
initialAgentRef={AGENT_REF}
|
|
260
|
+
initialInstanceId="inst_1"
|
|
261
|
+
/>,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const props = lastComposerProps();
|
|
265
|
+
expect(props.lockAgent).toBe(true);
|
|
266
|
+
expect(props.onMcpServerUsagesChange).toBeUndefined();
|
|
267
|
+
expect(props.onSkillRefsChange).toBeUndefined();
|
|
268
|
+
expect(props.sessionVariables).toBeUndefined();
|
|
269
|
+
// Guest-only restrictions on top of the endUser curation.
|
|
270
|
+
expect(props.showHarnessSelector).toBe(false);
|
|
271
|
+
expect(props.showInteractionModePicker).toBe(false);
|
|
272
|
+
expect(props.showModelSelector).toBe(false);
|
|
273
|
+
expect(props.enableAttachments).toBe(false);
|
|
274
|
+
expect(props.enableGitHub).toBe(false);
|
|
275
|
+
expect(props.workspace).toBeUndefined();
|
|
276
|
+
// The session panel's only toggle is absent for guests.
|
|
277
|
+
expect(screen.queryByRole("button", { name: "Show panel" })).toBeNull();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("guest: agent binding bypasses the composer's picker machinery", () => {
|
|
281
|
+
render(
|
|
282
|
+
<NewSessionViewer
|
|
283
|
+
org="acme"
|
|
284
|
+
onSessionCreated={vi.fn()}
|
|
285
|
+
audience="guest"
|
|
286
|
+
initialAgentRef={AGENT_REF}
|
|
287
|
+
initialInstanceId="inst_1"
|
|
288
|
+
/>,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// The composer's agent machinery is fully unwired (no picker, no
|
|
292
|
+
// resolution flow — those perform org reads a guest token cannot make)…
|
|
293
|
+
const props = lastComposerProps();
|
|
294
|
+
expect(props.onAgentRefChange).toBeUndefined();
|
|
295
|
+
expect(props.onAgentResolutionChange).toBeUndefined();
|
|
296
|
+
expect(props.initialAgentRef).toBeUndefined();
|
|
297
|
+
expect(props.initialInstanceId).toBeUndefined();
|
|
298
|
+
// …and the launcher pins the flow to the shared instance directly.
|
|
299
|
+
expect(stubNewSessionFlow.setAgentRef).toHaveBeenCalledWith(AGENT_REF);
|
|
300
|
+
expect(stubNewSessionFlow.setResolution).toHaveBeenCalledWith({
|
|
301
|
+
mode: "saved",
|
|
302
|
+
instanceId: "inst_1",
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("guest: forwards the audience to the flow", () => {
|
|
307
|
+
render(
|
|
308
|
+
<NewSessionViewer
|
|
309
|
+
org="acme"
|
|
310
|
+
onSessionCreated={vi.fn()}
|
|
311
|
+
audience="guest"
|
|
312
|
+
initialAgentRef={AGENT_REF}
|
|
313
|
+
initialInstanceId="inst_1"
|
|
314
|
+
/>,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
expect(mockUseNewSessionFlow).toHaveBeenCalledWith(
|
|
318
|
+
expect.objectContaining({ audience: "guest" }),
|
|
319
|
+
);
|
|
320
|
+
});
|
|
252
321
|
});
|
|
253
322
|
|
|
254
323
|
describe("SessionViewer — audience wiring", () => {
|
|
@@ -276,6 +345,36 @@ describe("SessionViewer — audience wiring", () => {
|
|
|
276
345
|
expect(openedConfigFacet().mutations).toBeUndefined();
|
|
277
346
|
});
|
|
278
347
|
|
|
348
|
+
it("guest: pure chat — every picker hidden, attachments and workspace off", () => {
|
|
349
|
+
render(<SessionViewer sessionId="ses_1" org="acme" audience="guest" />);
|
|
350
|
+
|
|
351
|
+
const props = lastComposerProps();
|
|
352
|
+
expect(props.lockAgent).toBe(true);
|
|
353
|
+
expect(props.onMcpServerUsagesChange).toBeUndefined();
|
|
354
|
+
expect(props.onSkillRefsChange).toBeUndefined();
|
|
355
|
+
expect(props.sessionVariables).toBeUndefined();
|
|
356
|
+
// Guest-only restrictions on top of the endUser curation.
|
|
357
|
+
expect(props.showInteractionModePicker).toBe(false);
|
|
358
|
+
expect(props.showModelSelector).toBe(false);
|
|
359
|
+
expect(props.enableAttachments).toBe(false);
|
|
360
|
+
expect(props.enableGitHub).toBe(false);
|
|
361
|
+
expect(props.workspace).toBeUndefined();
|
|
362
|
+
// Agent machinery is fully unwired — the session's agent is bound
|
|
363
|
+
// server-side and its resolution reads are FGA-denied for guests.
|
|
364
|
+
expect(props.onAgentRefChange).toBeUndefined();
|
|
365
|
+
expect(props.onAgentResolutionChange).toBeUndefined();
|
|
366
|
+
// The session panel's only toggle is absent for guests.
|
|
367
|
+
expect(screen.queryByRole("button", { name: "Show panel" })).toBeNull();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("guest: forwards the audience to the flow", () => {
|
|
371
|
+
render(<SessionViewer sessionId="ses_1" org="acme" audience="guest" />);
|
|
372
|
+
|
|
373
|
+
expect(mockUseSessionPageFlow).toHaveBeenCalledWith(
|
|
374
|
+
expect.objectContaining({ audience: "guest" }),
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
279
378
|
it("forwards getRuntimeEnv to the flow", () => {
|
|
280
379
|
const getRuntimeEnv = vi.fn();
|
|
281
380
|
render(
|
|
@@ -36,8 +36,9 @@ const mockDefaultAgent = {
|
|
|
36
36
|
refetch: vi.fn(),
|
|
37
37
|
waitForResolution: vi.fn<() => Promise<unknown>>(),
|
|
38
38
|
};
|
|
39
|
+
const useDefaultAgentSpy = vi.fn((_org: string | null) => mockDefaultAgent);
|
|
39
40
|
vi.mock("../../agent", () => ({
|
|
40
|
-
useDefaultAgent: () =>
|
|
41
|
+
useDefaultAgent: (org: string | null) => useDefaultAgentSpy(org),
|
|
41
42
|
}));
|
|
42
43
|
|
|
43
44
|
const mockWorkspace = {
|
|
@@ -683,4 +684,77 @@ describe("useNewSessionFlow", () => {
|
|
|
683
684
|
expect(mockDefaultAgent.waitForResolution).not.toHaveBeenCalled();
|
|
684
685
|
});
|
|
685
686
|
});
|
|
687
|
+
|
|
688
|
+
describe("guest audience", () => {
|
|
689
|
+
it("disables the default-agent lookup (a read guests cannot make)", () => {
|
|
690
|
+
const opts = { ...defaultOptions(), audience: "guest" as const };
|
|
691
|
+
renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
692
|
+
|
|
693
|
+
expect(useDefaultAgentSpy).toHaveBeenCalledWith(null);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("keeps the default-agent lookup for other audiences", () => {
|
|
697
|
+
renderHook(() => useNewSessionFlow(defaultOptions()), { wrapper: createWrapper() });
|
|
698
|
+
|
|
699
|
+
expect(useDefaultAgentSpy).toHaveBeenCalledWith("acme");
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("creates the session against the pinned resolution", async () => {
|
|
703
|
+
const opts = { ...defaultOptions(), audience: "guest" as const };
|
|
704
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
705
|
+
|
|
706
|
+
act(() => {
|
|
707
|
+
result.current.setAgentRef({ org: "acme", slug: "support-bot" });
|
|
708
|
+
result.current.setResolution({ mode: "saved", instanceId: "shared-inst" });
|
|
709
|
+
});
|
|
710
|
+
await act(async () => {
|
|
711
|
+
await result.current.submit("Hello");
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
expect(mockCreateSession).toHaveBeenCalledOnce();
|
|
715
|
+
expect(mockCreateSession.mock.calls[0][0].agentInstanceId).toBe("shared-inst");
|
|
716
|
+
expect(opts.onSessionCreated).toHaveBeenCalledWith("sess-new");
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("fails closed on submit without a resolution — never the default agent", async () => {
|
|
720
|
+
// A default agent exists (the beforeEach seeds one); a guest must
|
|
721
|
+
// still never fall back to it.
|
|
722
|
+
const opts = { ...defaultOptions(), audience: "guest" as const };
|
|
723
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
724
|
+
|
|
725
|
+
await act(async () => {
|
|
726
|
+
await result.current.submit("Hello");
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
expect(mockCreateSession).not.toHaveBeenCalled();
|
|
730
|
+
expect(mockCreateExecution).not.toHaveBeenCalled();
|
|
731
|
+
expect(result.current.submitError).toContain("still loading");
|
|
732
|
+
expect(opts.onError).toHaveBeenCalled();
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it("surfaces launch-gate refusal copy verbatim from the status description", async () => {
|
|
736
|
+
// The backend resolves owner-customizable refusal copy server-side and
|
|
737
|
+
// carries it in the gRPC status description — the flow must hand it to
|
|
738
|
+
// onError untouched (no client-side mapping exists by design).
|
|
739
|
+
const ownerCopy = "This agent is currently unavailable. Please check back later.";
|
|
740
|
+
const { ConnectError, Code } = await import("@connectrpc/connect");
|
|
741
|
+
mockCreateExecution.mockRejectedValueOnce(
|
|
742
|
+
new ConnectError(ownerCopy, Code.FailedPrecondition),
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
const opts = { ...defaultOptions(), audience: "guest" as const };
|
|
746
|
+
const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
|
|
747
|
+
|
|
748
|
+
act(() => {
|
|
749
|
+
result.current.setAgentRef({ org: "acme", slug: "support-bot" });
|
|
750
|
+
result.current.setResolution({ mode: "saved", instanceId: "shared-inst" });
|
|
751
|
+
});
|
|
752
|
+
await act(async () => {
|
|
753
|
+
await result.current.submit("Hello");
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
expect(result.current.submitError).toBe(ownerCopy);
|
|
757
|
+
expect(opts.onError).toHaveBeenCalledWith(ownerCopy);
|
|
758
|
+
});
|
|
759
|
+
});
|
|
686
760
|
});
|