@stigmer/react 3.12.6 → 3.12.7
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/composer/ComposerToolbar.d.ts +19 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +7 -2
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +10 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +2 -2
- package/composer/SessionComposer.js.map +1 -1
- package/composer/index.d.ts +1 -0
- package/composer/index.d.ts.map +1 -1
- package/composer/index.js.map +1 -1
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +8 -11
- package/execution/MessageThread.js.map +1 -1
- package/execution/useFileChangeContent.d.ts +2 -6
- package/execution/useFileChangeContent.d.ts.map +1 -1
- package/execution/useFileChangeContent.js +4 -12
- package/execution/useFileChangeContent.js.map +1 -1
- package/index.d.ts +6 -3
- package/index.d.ts.map +1 -1
- package/index.js +5 -1
- package/index.js.map +1 -1
- package/internal/download.d.ts +8 -0
- package/internal/download.d.ts.map +1 -0
- package/internal/download.js +25 -0
- package/internal/download.js.map +1 -0
- package/library/useExportResource.d.ts.map +1 -1
- package/library/useExportResource.js +3 -17
- package/library/useExportResource.js.map +1 -1
- package/memory/MemoryListPanel.d.ts +35 -0
- package/memory/MemoryListPanel.d.ts.map +1 -0
- package/memory/MemoryListPanel.js +141 -0
- package/memory/MemoryListPanel.js.map +1 -0
- package/memory/index.d.ts +8 -0
- package/memory/index.d.ts.map +1 -0
- package/memory/index.js +12 -0
- package/memory/index.js.map +1 -0
- package/memory/memoryGroups.d.ts +31 -0
- package/memory/memoryGroups.d.ts.map +1 -0
- package/memory/memoryGroups.js +52 -0
- package/memory/memoryGroups.js.map +1 -0
- package/memory/useConfirmMemory.d.ts +31 -0
- package/memory/useConfirmMemory.d.ts.map +1 -0
- package/memory/useConfirmMemory.js +43 -0
- package/memory/useConfirmMemory.js.map +1 -0
- package/memory/useDeleteMemory.d.ts +30 -0
- package/memory/useDeleteMemory.d.ts.map +1 -0
- package/memory/useDeleteMemory.js +42 -0
- package/memory/useDeleteMemory.js.map +1 -0
- package/memory/useMemories.d.ts +33 -0
- package/memory/useMemories.d.ts.map +1 -0
- package/memory/useMemories.js +42 -0
- package/memory/useMemories.js.map +1 -0
- package/memory/useRejectMemory.d.ts +34 -0
- package/memory/useRejectMemory.d.ts.map +1 -0
- package/memory/useRejectMemory.js +46 -0
- package/memory/useRejectMemory.js.map +1 -0
- package/memory/useUpdateMemoryContent.d.ts +32 -0
- package/memory/useUpdateMemoryContent.d.ts.map +1 -0
- package/memory/useUpdateMemoryContent.js +48 -0
- package/memory/useUpdateMemoryContent.js.map +1 -0
- package/package.json +4 -4
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +9 -1
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +14 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +21 -25
- package/session/SessionViewer.js.map +1 -1
- package/session/TranscriptExportMenu.d.ts +24 -0
- package/session/TranscriptExportMenu.d.ts.map +1 -0
- package/session/TranscriptExportMenu.js +26 -0
- package/session/TranscriptExportMenu.js.map +1 -0
- package/session/index.d.ts +4 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js +2 -0
- package/session/index.js.map +1 -1
- package/session/useExportTranscript.d.ts +44 -0
- package/session/useExportTranscript.d.ts.map +1 -0
- package/session/useExportTranscript.js +109 -0
- package/session/useExportTranscript.js.map +1 -0
- package/session/useNewSessionFlow.d.ts +11 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +14 -6
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +7 -19
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionExecutions.d.ts +2 -14
- package/session/useSessionExecutions.d.ts.map +1 -1
- package/session/useSessionExecutions.js +4 -26
- package/session/useSessionExecutions.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +23 -10
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +61 -9
- package/session/useSessionPageFlow.js.map +1 -1
- package/settings/MemorySection.d.ts +12 -0
- package/settings/MemorySection.d.ts.map +1 -0
- package/settings/MemorySection.js +21 -0
- package/settings/MemorySection.js.map +1 -0
- package/settings/index.d.ts +1 -0
- package/settings/index.d.ts.map +1 -1
- package/settings/index.js +1 -0
- package/settings/index.js.map +1 -1
- package/settings/settings-nav.d.ts.map +1 -1
- package/settings/settings-nav.js +2 -1
- package/settings/settings-nav.js.map +1 -1
- package/src/__tests__/public-api.test.ts +21 -0
- package/src/composer/ComposerToolbar.tsx +58 -0
- package/src/composer/SessionComposer.tsx +13 -1
- package/src/composer/__tests__/ComposerToolbar-autoApprove.test.tsx +80 -0
- package/src/composer/index.ts +1 -0
- package/src/execution/MessageThread.tsx +8 -11
- package/src/execution/useFileChangeContent.ts +5 -12
- package/src/index.ts +34 -0
- package/src/internal/download.ts +29 -0
- package/src/library/useExportResource.ts +3 -19
- package/src/memory/MemoryListPanel.tsx +532 -0
- package/src/memory/__tests__/MemoryListPanel.test.tsx +259 -0
- package/src/memory/__tests__/memoryHooks.test.tsx +220 -0
- package/src/memory/index.ts +19 -0
- package/src/memory/memoryGroups.ts +64 -0
- package/src/memory/useConfirmMemory.ts +62 -0
- package/src/memory/useDeleteMemory.ts +61 -0
- package/src/memory/useMemories.ts +66 -0
- package/src/memory/useRejectMemory.ts +65 -0
- package/src/memory/useUpdateMemoryContent.ts +67 -0
- package/src/session/NewSessionViewer.tsx +15 -1
- package/src/session/SessionViewer.tsx +48 -49
- package/src/session/TranscriptExportMenu.tsx +78 -0
- package/src/session/__tests__/SessionViewer-autoApproveToggle.test.tsx +178 -0
- package/src/session/__tests__/SessionViewer-transcriptExport.test.tsx +151 -0
- package/src/session/__tests__/TranscriptExportMenu.test.tsx +97 -0
- package/src/session/__tests__/useExportTranscript.test.tsx +187 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +33 -0
- package/src/session/__tests__/useSessionPageFlow.autoApprove.test.tsx +176 -2
- package/src/session/index.ts +9 -0
- package/src/session/useExportTranscript.ts +171 -0
- package/src/session/useNewSessionFlow.ts +28 -7
- package/src/session/useSessionConversation.ts +10 -16
- package/src/session/useSessionExecutions.ts +5 -26
- package/src/session/useSessionPageFlow.ts +84 -21
- package/src/settings/MemorySection.tsx +45 -0
- package/src/settings/index.ts +1 -0
- package/src/settings/settings-nav.ts +2 -0
- package/src/usage/useExportCSV.ts +2 -14
- package/usage/useExportCSV.d.ts.map +1 -1
- package/usage/useExportCSV.js +2 -13
- package/usage/useExportCSV.js.map +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
|
|
5
|
+
import { ListMemoriesRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/io_pb";
|
|
6
|
+
import { useStigmer } from "../hooks.js";
|
|
7
|
+
import { useFetch } from "../internal/useFetch.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The server-enforced per-subject-per-org record ceiling (all lifecycle
|
|
11
|
+
* states). Requesting one page of exactly this size fetches EVERY record
|
|
12
|
+
* the caller can see, by construction — the cap is why this list needs
|
|
13
|
+
* no pagination UI.
|
|
14
|
+
*/
|
|
15
|
+
const MEMORY_CAP = 100;
|
|
16
|
+
|
|
17
|
+
/** Return value of {@link useMemories}. */
|
|
18
|
+
export interface UseMemoriesReturn {
|
|
19
|
+
/** Every memory the caller can view in the org, newest first. */
|
|
20
|
+
readonly memories: readonly Memory[];
|
|
21
|
+
/** `true` while the initial fetch is in flight. */
|
|
22
|
+
readonly isLoading: boolean;
|
|
23
|
+
/** `true` while a background refetch is in flight. */
|
|
24
|
+
readonly isRefetching: boolean;
|
|
25
|
+
/** Error from the last failed request, or `null` when healthy. */
|
|
26
|
+
readonly error: Error | null;
|
|
27
|
+
/** Discard cached data and re-fetch the list from the server. */
|
|
28
|
+
readonly refetch: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Data hook that fetches the caller's memories in an organization.
|
|
33
|
+
*
|
|
34
|
+
* Memory content is subject-only: the server returns exactly the records
|
|
35
|
+
* that are about the caller (FGA-filtered in cloud; single-user in local
|
|
36
|
+
* mode), so this list is always "what the platform remembers about ME
|
|
37
|
+
* here". Ordering is newest first; group by lifecycle state for display
|
|
38
|
+
* with {@link groupMemoriesByLifecycle}.
|
|
39
|
+
*
|
|
40
|
+
* Pass `null` as `org` to skip fetching (stable no-op).
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* const { memories, isLoading, refetch } = useMemories(org);
|
|
45
|
+
* const groups = groupMemoriesByLifecycle(memories);
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function useMemories(org: string | null): UseMemoriesReturn {
|
|
49
|
+
const stigmer = useStigmer();
|
|
50
|
+
|
|
51
|
+
const { data: memories, isLoading, isRefetching, error, refetch } = useFetch(
|
|
52
|
+
org
|
|
53
|
+
? () =>
|
|
54
|
+
stigmer.memory
|
|
55
|
+
.list(create(ListMemoriesRequestSchema, {
|
|
56
|
+
org,
|
|
57
|
+
pageInfo: { num: 1, size: MEMORY_CAP },
|
|
58
|
+
}))
|
|
59
|
+
.then((r) => [...r.items])
|
|
60
|
+
: null,
|
|
61
|
+
[stigmer, org],
|
|
62
|
+
[] as Memory[],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return { memories, isLoading, isRefetching, error, refetch };
|
|
66
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
|
|
5
|
+
import { useStigmer } from "../hooks.js";
|
|
6
|
+
import { toError } from "../internal/toError.js";
|
|
7
|
+
|
|
8
|
+
/** Return value of {@link useRejectMemory}. */
|
|
9
|
+
export interface UseRejectMemoryReturn {
|
|
10
|
+
/** Reject a proposed memory by its resource ID. Resolves with the rejected record. */
|
|
11
|
+
readonly rejectMemory: (id: string) => Promise<Memory>;
|
|
12
|
+
/** `true` while the reject request is in flight. */
|
|
13
|
+
readonly isRejecting: boolean;
|
|
14
|
+
/** Error from the last failed reject, or `null` when healthy. */
|
|
15
|
+
readonly error: Error | null;
|
|
16
|
+
/** Reset `error` to `null`. */
|
|
17
|
+
readonly clearError: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Behavior hook that wraps `memory.reject()`. A rejected memory is never
|
|
22
|
+
* recalled; the record is kept as an audit trail until deleted.
|
|
23
|
+
*
|
|
24
|
+
* Rejecting an already-rejected memory succeeds and changes nothing.
|
|
25
|
+
* Rejecting a confirmed memory fails with FAILED_PRECONDITION —
|
|
26
|
+
* deleting a confirmed memory is how it is revoked.
|
|
27
|
+
*
|
|
28
|
+
* Rejection is deliberately one click on every surface, with no
|
|
29
|
+
* confirmation dialog (DD-005 D4): expensive review teaches users to
|
|
30
|
+
* ignore the proposal queue.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* const { rejectMemory, isRejecting } = useRejectMemory();
|
|
35
|
+
*
|
|
36
|
+
* await rejectMemory(memory.metadata?.id ?? "");
|
|
37
|
+
* refetch(); // refresh the list after the decision
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function useRejectMemory(): UseRejectMemoryReturn {
|
|
41
|
+
const stigmer = useStigmer();
|
|
42
|
+
const [isRejecting, setIsRejecting] = useState(false);
|
|
43
|
+
const [error, setError] = useState<Error | null>(null);
|
|
44
|
+
|
|
45
|
+
const clearError = useCallback(() => setError(null), []);
|
|
46
|
+
|
|
47
|
+
const rejectMemory = useCallback(
|
|
48
|
+
async (id: string): Promise<Memory> => {
|
|
49
|
+
setIsRejecting(true);
|
|
50
|
+
setError(null);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
return await stigmer.memory.reject(id);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
setError(toError(err));
|
|
56
|
+
throw err;
|
|
57
|
+
} finally {
|
|
58
|
+
setIsRejecting(false);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[stigmer],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return { rejectMemory, isRejecting, error, clearError };
|
|
65
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
|
|
5
|
+
import { toMemoryUpdateInput } from "@stigmer/sdk";
|
|
6
|
+
import { useStigmer } from "../hooks.js";
|
|
7
|
+
import { toError } from "../internal/toError.js";
|
|
8
|
+
|
|
9
|
+
/** Return value of {@link useUpdateMemoryContent}. */
|
|
10
|
+
export interface UseUpdateMemoryContentReturn {
|
|
11
|
+
/** Replace a memory's fact text. Resolves with the updated record. */
|
|
12
|
+
readonly updateContent: (memory: Memory, content: string) => Promise<Memory>;
|
|
13
|
+
/** `true` while the update request is in flight. */
|
|
14
|
+
readonly isUpdating: boolean;
|
|
15
|
+
/** Error from the last failed update, or `null` when healthy. */
|
|
16
|
+
readonly error: Error | null;
|
|
17
|
+
/** Reset `error` to `null`. */
|
|
18
|
+
readonly clearError: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Behavior hook that edits a memory's fact text — the one field the
|
|
23
|
+
* subject owns after capture (DD-004).
|
|
24
|
+
*
|
|
25
|
+
* The update RPC replaces the entire spec, so the loaded record is
|
|
26
|
+
* mapped through the generated `toMemoryUpdateInput` and only `content`
|
|
27
|
+
* is overridden — the wipe-safe editing rule. Subject and provenance
|
|
28
|
+
* ride along verbatim (they are update-immutable server-side), and the
|
|
29
|
+
* consent lifecycle is never touched by updates.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* const { updateContent, isUpdating } = useUpdateMemoryContent();
|
|
34
|
+
*
|
|
35
|
+
* await updateContent(memory, editedText);
|
|
36
|
+
* refetch(); // refresh the list after the edit
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function useUpdateMemoryContent(): UseUpdateMemoryContentReturn {
|
|
40
|
+
const stigmer = useStigmer();
|
|
41
|
+
const [isUpdating, setIsUpdating] = useState(false);
|
|
42
|
+
const [error, setError] = useState<Error | null>(null);
|
|
43
|
+
|
|
44
|
+
const clearError = useCallback(() => setError(null), []);
|
|
45
|
+
|
|
46
|
+
const updateContent = useCallback(
|
|
47
|
+
async (memory: Memory, content: string): Promise<Memory> => {
|
|
48
|
+
setIsUpdating(true);
|
|
49
|
+
setError(null);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
return await stigmer.memory.update({
|
|
53
|
+
...toMemoryUpdateInput(memory),
|
|
54
|
+
content,
|
|
55
|
+
});
|
|
56
|
+
} catch (err) {
|
|
57
|
+
setError(toError(err));
|
|
58
|
+
throw err;
|
|
59
|
+
} finally {
|
|
60
|
+
setIsUpdating(false);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
[stigmer],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return { updateContent, isUpdating, error, clearError };
|
|
67
|
+
}
|
|
@@ -6,7 +6,7 @@ import type { UseGitHubConnectionReturn } from "../github/useGitHubConnection.js
|
|
|
6
6
|
import type { WorkspaceFileLister } from "../workspace/WorkspaceFileLister.js";
|
|
7
7
|
import type { WorkspaceFileReader } from "../workspace/WorkspaceFileReader.js";
|
|
8
8
|
import type { WorkspaceContentSearcher } from "../workspace/WorkspaceContentSearcher.js";
|
|
9
|
-
import type { InteractionModeOption } from "../composer/index.js";
|
|
9
|
+
import type { ComposerAutoApproveProps, InteractionModeOption } from "../composer/index.js";
|
|
10
10
|
import { SessionComposer } from "../composer/index.js";
|
|
11
11
|
import type { HarnessOption } from "../models/harness.js";
|
|
12
12
|
import type { AccountExecutionDefaults } from "../identity-account/useAccountExecutionDefaults.js";
|
|
@@ -299,6 +299,19 @@ export function NewSessionViewer({
|
|
|
299
299
|
// SessionViewer derives (DD-016), gating the chip and the region together.
|
|
300
300
|
const panelEnabled = panelMode !== "none" && !isGuest;
|
|
301
301
|
|
|
302
|
+
// Always-visible auto-approve toggle (#816), present before the first
|
|
303
|
+
// message is ever typed — the walk-away user arms it here and the create
|
|
304
|
+
// carries spec.auto_approve_all server-side. Guests never get it (the
|
|
305
|
+
// operator-consent reasoning as the #302 host default). Memoized per
|
|
306
|
+
// DD-010 — the composer is memo'd and an inline object would defeat it.
|
|
307
|
+
const autoApprove = useMemo<ComposerAutoApproveProps | undefined>(
|
|
308
|
+
() =>
|
|
309
|
+
isGuest
|
|
310
|
+
? undefined
|
|
311
|
+
: { armed: flow.autoApproveAll, onChange: flow.setAutoApproveAll },
|
|
312
|
+
[isGuest, flow.autoApproveAll, flow.setAutoApproveAll],
|
|
313
|
+
);
|
|
314
|
+
|
|
302
315
|
// Guest agent binding is host configuration, not a picker interaction:
|
|
303
316
|
// the composer's agent machinery (picker, env-collection, personal
|
|
304
317
|
// environments — all org reads a guest token cannot make) stays fully
|
|
@@ -452,6 +465,7 @@ export function NewSessionViewer({
|
|
|
452
465
|
onInteractionModeChange={setInteractionMode}
|
|
453
466
|
showInteractionModePicker={!isGuest}
|
|
454
467
|
showModelSelector={modelSelectorVisible}
|
|
468
|
+
autoApprove={autoApprove}
|
|
455
469
|
enableAttachments={!isGuest}
|
|
456
470
|
defaultModelId={flow.modelId}
|
|
457
471
|
onModelChange={flow.setModelId}
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
WorkspaceSurface,
|
|
18
18
|
type SurfaceVirtualDocument,
|
|
19
19
|
} from "../workspace/WorkspaceSurface.js";
|
|
20
|
-
import type { InteractionModeOption, SessionComposerHandle, SessionComposerSubmitContext } from "../composer/index.js";
|
|
20
|
+
import type { ComposerAutoApproveProps, InteractionModeOption, SessionComposerHandle, SessionComposerSubmitContext } from "../composer/index.js";
|
|
21
21
|
import type { ApplyResourceResult } from "../library/useApplyResource.js";
|
|
22
22
|
import { SessionViewerLayout } from "./SessionViewerLayout.js";
|
|
23
23
|
import { useWorkspaceEditors, isVirtualEntryId } from "../internal/store/index.js";
|
|
@@ -51,6 +51,7 @@ import { ArtifactDocument } from "../execution/ArtifactDocument.js";
|
|
|
51
51
|
import { useSessionRailViews } from "./useSessionRailViews.js";
|
|
52
52
|
import { useSessionPanel, type SessionPanelController } from "./useSessionPanel.js";
|
|
53
53
|
import { SessionPanelChip } from "./SessionPanelChip.js";
|
|
54
|
+
import { TranscriptExportMenu } from "./TranscriptExportMenu.js";
|
|
54
55
|
import { useSessionWriteBacks } from "./useSessionWriteBacks.js";
|
|
55
56
|
import { useWorkspaceReadRefs } from "./useWorkspaceReadRefs.js";
|
|
56
57
|
import {
|
|
@@ -293,6 +294,19 @@ export interface SessionViewerProps {
|
|
|
293
294
|
* unopinionated about Console auth (DD-004).
|
|
294
295
|
*/
|
|
295
296
|
readonly headerActions?: ReactNode;
|
|
297
|
+
/**
|
|
298
|
+
* Whether the viewer renders its built-in whole-conversation export
|
|
299
|
+
* control ({@link TranscriptExportMenu}) in the header corner
|
|
300
|
+
* (stigmer/stigmer#814). Default ON — a deliberate DD-011 divergence,
|
|
301
|
+
* owner-ratified: wherever a conversation can be viewed its transcript
|
|
302
|
+
* should be one click away, the control is an additive header button (no
|
|
303
|
+
* rendering-strategy or DOM-structure change to existing content), and
|
|
304
|
+
* the export carries only what the current caller can already read.
|
|
305
|
+
* Hosts that own their export surface (or inert demo scenes) opt out.
|
|
306
|
+
*
|
|
307
|
+
* @default true
|
|
308
|
+
*/
|
|
309
|
+
readonly transcriptExport?: boolean;
|
|
296
310
|
/**
|
|
297
311
|
* Host-injected access management control (e.g. the Console's
|
|
298
312
|
* `ManageAccessButton` with its own permission gating). Rendered inside
|
|
@@ -374,6 +388,7 @@ export function SessionViewer({
|
|
|
374
388
|
panelOpen,
|
|
375
389
|
onPanelOpenChange,
|
|
376
390
|
headerActions,
|
|
391
|
+
transcriptExport = true,
|
|
377
392
|
accessSlot,
|
|
378
393
|
onApplied,
|
|
379
394
|
threadSlots,
|
|
@@ -646,13 +661,23 @@ export function SessionViewer({
|
|
|
646
661
|
return (
|
|
647
662
|
<SessionViewerLayout
|
|
648
663
|
className={className}
|
|
649
|
-
// Top-right controls: host actions + the
|
|
650
|
-
// panel's always-mounted toggle; while
|
|
651
|
-
// pending-item count. Execution status
|
|
652
|
-
// chrome — the thread itself communicates
|
|
653
|
-
// a panel (guests — session configuration
|
|
654
|
-
// — or a host's panel="none") has no
|
|
655
|
-
|
|
664
|
+
// Top-right controls: host actions + the built-in transcript export +
|
|
665
|
+
// the panel chip. The chip is the panel's always-mounted toggle; while
|
|
666
|
+
// collapsed it carries only the pending-item count. Execution status
|
|
667
|
+
// is never surfaced as header chrome — the thread itself communicates
|
|
668
|
+
// run state. A viewer without a panel (guests — session configuration
|
|
669
|
+
// is not a visitor's business — or a host's panel="none") has no
|
|
670
|
+
// toggle: it is simply absent.
|
|
671
|
+
headerActions={
|
|
672
|
+
transcriptExport ? (
|
|
673
|
+
<>
|
|
674
|
+
{headerActions}
|
|
675
|
+
<TranscriptExportMenu sessionId={sessionId} />
|
|
676
|
+
</>
|
|
677
|
+
) : (
|
|
678
|
+
headerActions
|
|
679
|
+
)
|
|
680
|
+
}
|
|
656
681
|
chip={
|
|
657
682
|
panelEnabled ? (
|
|
658
683
|
<SessionPanelChip
|
|
@@ -824,6 +849,20 @@ const ConversationColumn = memo(function ConversationColumn({
|
|
|
824
849
|
void conv.stop();
|
|
825
850
|
}, [conv.stop]);
|
|
826
851
|
|
|
852
|
+
// Always-visible auto-approve toggle (#816), replacing the old armed-only
|
|
853
|
+
// indicator banner: the walk-away user needs a way ON before any gate
|
|
854
|
+
// exists, not just a way off after one. Guests never get it (they don't
|
|
855
|
+
// inherit the host default and this is the operator's consent surface);
|
|
856
|
+
// observers have no composer at all. Memoized per DD-010 — the composer
|
|
857
|
+
// is memo'd and an inline object would defeat it.
|
|
858
|
+
const autoApprove = useMemo<ComposerAutoApproveProps | undefined>(
|
|
859
|
+
() =>
|
|
860
|
+
isGuest
|
|
861
|
+
? undefined
|
|
862
|
+
: { armed: flow.autoApproveAll, onChange: flow.setAutoApproveAll },
|
|
863
|
+
[isGuest, flow.autoApproveAll, flow.setAutoApproveAll],
|
|
864
|
+
);
|
|
865
|
+
|
|
827
866
|
// Edit-and-resubmit: stop the in-flight turn, pre-fill the composer with
|
|
828
867
|
// the original text, and remember which execution is being edited. The
|
|
829
868
|
// append-only execution log is never rewritten — submitting while editing
|
|
@@ -948,9 +987,6 @@ const ConversationColumn = memo(function ConversationColumn({
|
|
|
948
987
|
/>
|
|
949
988
|
)}
|
|
950
989
|
{!isObserver && sendError && <SendErrorBanner error={sendError} />}
|
|
951
|
-
{!isObserver && flow.autoApproveAll && (
|
|
952
|
-
<AutoApproveIndicator onTurnOff={() => flow.setAutoApproveAll(false)} />
|
|
953
|
-
)}
|
|
954
990
|
{/* Pending file reviews dock here — pinned above the composer so the
|
|
955
991
|
decision the agent is blocked on can never scroll out of view. The
|
|
956
992
|
thread renders only observational rows (badges) and read-only
|
|
@@ -992,6 +1028,7 @@ const ConversationColumn = memo(function ConversationColumn({
|
|
|
992
1028
|
onInteractionModeChange={setInteractionMode}
|
|
993
1029
|
showInteractionModePicker={!isGuest}
|
|
994
1030
|
showModelSelector={modelSelectorVisible}
|
|
1031
|
+
autoApprove={autoApprove}
|
|
995
1032
|
enableAttachments={!isGuest}
|
|
996
1033
|
workspace={isGuest ? undefined : flow.workspace}
|
|
997
1034
|
gitHubConnection={isGuest ? undefined : gitHubConnection}
|
|
@@ -1386,36 +1423,6 @@ function SessionStarting() {
|
|
|
1386
1423
|
);
|
|
1387
1424
|
}
|
|
1388
1425
|
|
|
1389
|
-
/**
|
|
1390
|
-
* Low-weight, always-visible indicator shown while the session-scoped
|
|
1391
|
-
* auto-approve preference is active. The "Turn off" control reverts the
|
|
1392
|
-
* preference in one click — the safety affordance for "Approve & don't ask
|
|
1393
|
-
* again". Visible from the first render when the host pre-armed the
|
|
1394
|
-
* preference via `StigmerProvider`'s `approvalDefaults` (#302); otherwise
|
|
1395
|
-
* nothing about approvals appears until the user opts in at a gate. Either
|
|
1396
|
-
* way the user keeps the last word.
|
|
1397
|
-
*/
|
|
1398
|
-
function AutoApproveIndicator({ onTurnOff }: { onTurnOff: () => void }) {
|
|
1399
|
-
return (
|
|
1400
|
-
<div
|
|
1401
|
-
role="status"
|
|
1402
|
-
className="stg:flex stg:items-center stg:gap-2 stg:border-t stg:border-border-muted stg:px-4 stg:py-1.5 stg:text-xs stg:text-muted-foreground"
|
|
1403
|
-
>
|
|
1404
|
-
<ShieldCheckIcon />
|
|
1405
|
-
<span className="stg:min-w-0 stg:flex-1 stg:truncate">
|
|
1406
|
-
Auto-approving tool calls for this session
|
|
1407
|
-
</span>
|
|
1408
|
-
<button
|
|
1409
|
-
type="button"
|
|
1410
|
-
onClick={onTurnOff}
|
|
1411
|
-
className="stg:shrink-0 stg:rounded stg:font-medium stg:text-foreground stg:underline-offset-2 stg:hover:underline stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-ring"
|
|
1412
|
-
>
|
|
1413
|
-
Turn off
|
|
1414
|
-
</button>
|
|
1415
|
-
</div>
|
|
1416
|
-
);
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
1426
|
/**
|
|
1420
1427
|
* The plan document tab's empty state — reachable only when a streaming plan
|
|
1421
1428
|
* auto-opened the tab and its turn then ended without publishing (stopped or
|
|
@@ -1616,11 +1623,3 @@ function LoaderIcon() {
|
|
|
1616
1623
|
);
|
|
1617
1624
|
}
|
|
1618
1625
|
|
|
1619
|
-
function ShieldCheckIcon() {
|
|
1620
|
-
return (
|
|
1621
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="stg:shrink-0 stg:text-success" aria-hidden="true">
|
|
1622
|
-
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
1623
|
-
<path d="m9 12 2 2 4-4" />
|
|
1624
|
-
</svg>
|
|
1625
|
-
);
|
|
1626
|
-
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Copy, Download, FileJson, FileText } from "lucide-react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { ActionMenu } from "../action-menu/ActionMenu.js";
|
|
6
|
+
import { SpinnerIcon } from "../internal/SpinnerIcon.js";
|
|
7
|
+
import {
|
|
8
|
+
useExportTranscript,
|
|
9
|
+
type UseExportTranscriptOptions,
|
|
10
|
+
} from "./useExportTranscript.js";
|
|
11
|
+
|
|
12
|
+
/** Props for {@link TranscriptExportMenu}. */
|
|
13
|
+
export interface TranscriptExportMenuProps extends UseExportTranscriptOptions {
|
|
14
|
+
/** The session whose conversation is exported. */
|
|
15
|
+
readonly sessionId: string | null;
|
|
16
|
+
/** Additional CSS classes for the trigger button. */
|
|
17
|
+
readonly className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The whole-conversation export control (stigmer/stigmer#814): a quiet
|
|
22
|
+
* icon-button menu offering **Copy transcript**, **Download Markdown**, and
|
|
23
|
+
* **Download JSON**, powered by {@link useExportTranscript} — the canonical
|
|
24
|
+
* transcript assembly, thinking and resolved tool outputs included.
|
|
25
|
+
*
|
|
26
|
+
* `SessionViewer` renders it in its header corner by default; it is exported
|
|
27
|
+
* so hosts can mount the same control on any other surface a conversation is
|
|
28
|
+
* reachable from (a session list's row menu, a custom header).
|
|
29
|
+
*
|
|
30
|
+
* The export serializes exactly what the viewer already shows the current
|
|
31
|
+
* caller — the RPCs behind it enforce the same `can_view` permission — so
|
|
32
|
+
* offering it to every audience adds convenience, not exposure.
|
|
33
|
+
*/
|
|
34
|
+
export function TranscriptExportMenu({
|
|
35
|
+
sessionId,
|
|
36
|
+
includeSuperseded,
|
|
37
|
+
className,
|
|
38
|
+
}: TranscriptExportMenuProps) {
|
|
39
|
+
const exporter = useExportTranscript(sessionId, { includeSuperseded });
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<ActionMenu>
|
|
43
|
+
<ActionMenu.Trigger
|
|
44
|
+
aria-label="Export transcript"
|
|
45
|
+
className={cn("stg:size-7", className)}
|
|
46
|
+
>
|
|
47
|
+
{exporter.isExporting ? (
|
|
48
|
+
<SpinnerIcon size={14} />
|
|
49
|
+
) : (
|
|
50
|
+
<Download className="stg:size-3.5" aria-hidden="true" />
|
|
51
|
+
)}
|
|
52
|
+
</ActionMenu.Trigger>
|
|
53
|
+
<ActionMenu.Content>
|
|
54
|
+
<ActionMenu.Item
|
|
55
|
+
icon={<Copy className="stg:size-3.5" aria-hidden="true" />}
|
|
56
|
+
onSelect={() => void exporter.copyMarkdown()}
|
|
57
|
+
disabled={exporter.isExporting}
|
|
58
|
+
>
|
|
59
|
+
Copy transcript
|
|
60
|
+
</ActionMenu.Item>
|
|
61
|
+
<ActionMenu.Item
|
|
62
|
+
icon={<FileText className="stg:size-3.5" aria-hidden="true" />}
|
|
63
|
+
onSelect={() => void exporter.downloadMarkdown()}
|
|
64
|
+
disabled={exporter.isExporting}
|
|
65
|
+
>
|
|
66
|
+
Download Markdown
|
|
67
|
+
</ActionMenu.Item>
|
|
68
|
+
<ActionMenu.Item
|
|
69
|
+
icon={<FileJson className="stg:size-3.5" aria-hidden="true" />}
|
|
70
|
+
onSelect={() => void exporter.downloadJson()}
|
|
71
|
+
disabled={exporter.isExporting}
|
|
72
|
+
>
|
|
73
|
+
Download JSON
|
|
74
|
+
</ActionMenu.Item>
|
|
75
|
+
</ActionMenu.Content>
|
|
76
|
+
</ActionMenu>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Wiring-contract tests for the always-visible auto-approve toggle
|
|
6
|
+
// (stigmer/stigmer#816): SessionViewer hands the composer the flow's
|
|
7
|
+
// auto-approve state for every audience that may submit approvals
|
|
8
|
+
// (integrator AND endUser — the walk-away persona is an embedder's end
|
|
9
|
+
// user), never for guests, and the old armed-only indicator banner is gone
|
|
10
|
+
// (the toggle IS the indicator now).
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
type CapturedProps = Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
const composerProps: CapturedProps[] = [];
|
|
16
|
+
vi.mock("../../composer", async (importOriginal) => {
|
|
17
|
+
const actual = await importOriginal<typeof import("../../composer")>();
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
SessionComposer: (props: CapturedProps) => {
|
|
21
|
+
composerProps.push(props);
|
|
22
|
+
return <div data-testid="composer-probe" />;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
vi.mock("../../execution/MessageThread", () => ({
|
|
28
|
+
MessageThread: () => <div data-testid="thread-probe" />,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock("../../execution/FileReviewDock", () => ({
|
|
32
|
+
FileReviewDock: () => <div data-testid="file-review-dock-probe" />,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("../facets/SetupTab", () => ({
|
|
36
|
+
SetupTab: () => <div data-testid="setup-probe" />,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const stubWorkspace = {
|
|
40
|
+
entries: [],
|
|
41
|
+
hasEntries: false,
|
|
42
|
+
toInput: vi.fn().mockReturnValue([]),
|
|
43
|
+
addGitRepo: vi.fn(),
|
|
44
|
+
addLocalPath: vi.fn(),
|
|
45
|
+
removeEntry: vi.fn(),
|
|
46
|
+
clear: vi.fn(),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const stubConv = {
|
|
50
|
+
session: { spec: {} } as Record<string, unknown>,
|
|
51
|
+
isLoading: false,
|
|
52
|
+
loadError: null,
|
|
53
|
+
completedExecutions: [],
|
|
54
|
+
activeStreamExecution: null,
|
|
55
|
+
activePhase: null,
|
|
56
|
+
isStreaming: false,
|
|
57
|
+
isConnecting: false,
|
|
58
|
+
sendFollowUp: vi.fn(),
|
|
59
|
+
canSendFollowUp: true,
|
|
60
|
+
isSending: false,
|
|
61
|
+
sendError: null,
|
|
62
|
+
clearSendError: vi.fn(),
|
|
63
|
+
pendingUserMessage: null,
|
|
64
|
+
workspaceEntries: [],
|
|
65
|
+
mcpServerUsages: [],
|
|
66
|
+
skillRefs: [],
|
|
67
|
+
pendingApprovals: [],
|
|
68
|
+
submitApproval: vi.fn(),
|
|
69
|
+
submittingApprovalIds: new Set<string>(),
|
|
70
|
+
fileChangeSets: [],
|
|
71
|
+
submitFileDecision: vi.fn(),
|
|
72
|
+
submittingFileDecisionKeys: new Set<string>(),
|
|
73
|
+
fileDecisionErrors: new Map<string, Error>(),
|
|
74
|
+
streamError: null,
|
|
75
|
+
reconnectStream: vi.fn(),
|
|
76
|
+
approvalError: null,
|
|
77
|
+
retryLastSend: vi.fn(),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const setAutoApproveAll = vi.fn();
|
|
81
|
+
const stubSessionPageFlow = {
|
|
82
|
+
conv: stubConv,
|
|
83
|
+
harness: "native" as const,
|
|
84
|
+
executionTarget: undefined,
|
|
85
|
+
model: [undefined, vi.fn()] as const,
|
|
86
|
+
interactionMode: ["agent" as const, vi.fn()] as const,
|
|
87
|
+
agentRef: { org: "acme", slug: "support-bot" },
|
|
88
|
+
setAgentRef: vi.fn(),
|
|
89
|
+
resolution: null,
|
|
90
|
+
setResolution: vi.fn(),
|
|
91
|
+
isDefaultAgent: false,
|
|
92
|
+
mcpServerUsages: [],
|
|
93
|
+
setMcpServerUsages: vi.fn(),
|
|
94
|
+
skillRefs: [],
|
|
95
|
+
setSkillRefs: vi.fn(),
|
|
96
|
+
workspace: stubWorkspace,
|
|
97
|
+
sessionVariables: { entries: [], isEmpty: true, clear: vi.fn() },
|
|
98
|
+
autoApproveAll: false,
|
|
99
|
+
setAutoApproveAll,
|
|
100
|
+
submitApproval: vi.fn(),
|
|
101
|
+
handleSubmit: vi.fn(),
|
|
102
|
+
submitError: null as Error | null,
|
|
103
|
+
displayExecution: null,
|
|
104
|
+
allExecutions: [],
|
|
105
|
+
sandboxWorkspaceRoot: undefined,
|
|
106
|
+
};
|
|
107
|
+
vi.mock("../useSessionPageFlow", () => ({
|
|
108
|
+
useSessionPageFlow: () => stubSessionPageFlow,
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
vi.mock("../../hooks", () => ({
|
|
112
|
+
useStigmer: () => ({
|
|
113
|
+
agentExecution: {
|
|
114
|
+
uploadAttachment: vi.fn(),
|
|
115
|
+
getArtifactContent: vi.fn(),
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
import { SessionViewer } from "../SessionViewer";
|
|
121
|
+
|
|
122
|
+
function lastComposerProps(): CapturedProps {
|
|
123
|
+
expect(composerProps.length).toBeGreaterThan(0);
|
|
124
|
+
return composerProps.at(-1)!;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
beforeEach(() => {
|
|
128
|
+
composerProps.length = 0;
|
|
129
|
+
stubConv.session = { spec: {} };
|
|
130
|
+
stubSessionPageFlow.autoApproveAll = false;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
cleanup();
|
|
135
|
+
vi.clearAllMocks();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("SessionViewer — auto-approve toggle wiring (#816)", () => {
|
|
139
|
+
it("hands the composer the flow's auto-approve state (integrator)", () => {
|
|
140
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
141
|
+
|
|
142
|
+
const autoApprove = lastComposerProps().autoApprove as
|
|
143
|
+
| { armed: boolean; onChange: (v: boolean) => void }
|
|
144
|
+
| undefined;
|
|
145
|
+
expect(autoApprove).toBeDefined();
|
|
146
|
+
expect(autoApprove!.armed).toBe(false);
|
|
147
|
+
expect(autoApprove!.onChange).toBe(setAutoApproveAll);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("mirrors an armed flow state into the composer", () => {
|
|
151
|
+
stubSessionPageFlow.autoApproveAll = true;
|
|
152
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
153
|
+
|
|
154
|
+
const autoApprove = lastComposerProps().autoApprove as { armed: boolean };
|
|
155
|
+
expect(autoApprove.armed).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("endUser gets the toggle — the walk-away persona is an end user", () => {
|
|
159
|
+
render(<SessionViewer sessionId="ses_1" org="acme" audience="endUser" />);
|
|
160
|
+
|
|
161
|
+
expect(lastComposerProps().autoApprove).toBeDefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("guests never get the toggle", () => {
|
|
165
|
+
render(<SessionViewer sessionId="ses_1" org="acme" audience="guest" />);
|
|
166
|
+
|
|
167
|
+
expect(lastComposerProps().autoApprove).toBeUndefined();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("the armed-only indicator banner is gone — the toggle is the indicator", () => {
|
|
171
|
+
stubSessionPageFlow.autoApproveAll = true;
|
|
172
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
173
|
+
|
|
174
|
+
expect(
|
|
175
|
+
screen.queryByText("Auto-approving tool calls for this session"),
|
|
176
|
+
).toBeNull();
|
|
177
|
+
});
|
|
178
|
+
});
|