@stigmer/react 3.1.4 → 3.1.6
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/execution/FileReviewContext.d.ts +15 -1
- package/execution/FileReviewContext.d.ts.map +1 -1
- package/execution/FileReviewContext.js +20 -1
- package/execution/FileReviewContext.js.map +1 -1
- package/execution/MessageThread.d.ts +65 -2
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +42 -23
- package/execution/MessageThread.js.map +1 -1
- package/execution/TodoCard.d.ts +8 -0
- package/execution/TodoCard.d.ts.map +1 -1
- package/execution/TodoCard.js +6 -4
- package/execution/TodoCard.js.map +1 -1
- package/execution/TodoList.d.ts +20 -1
- package/execution/TodoList.d.ts.map +1 -1
- package/execution/TodoList.js +9 -6
- package/execution/TodoList.js.map +1 -1
- package/execution/ToolCallDetail.d.ts +3 -2
- package/execution/ToolCallDetail.d.ts.map +1 -1
- package/execution/ToolCallDetail.js +49 -16
- package/execution/ToolCallDetail.js.map +1 -1
- package/execution/WriteBackCard.d.ts +7 -1
- package/execution/WriteBackCard.d.ts.map +1 -1
- package/execution/WriteBackCard.js +10 -2
- package/execution/WriteBackCard.js.map +1 -1
- package/execution/file-review-status.d.ts +15 -0
- package/execution/file-review-status.d.ts.map +1 -1
- package/execution/file-review-status.js +22 -0
- package/execution/file-review-status.js.map +1 -1
- package/execution/index.d.ts +5 -5
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +3 -3
- package/execution/index.js.map +1 -1
- package/github/useGitHubFileReader.d.ts +4 -2
- package/github/useGitHubFileReader.d.ts.map +1 -1
- package/github/useGitHubFileReader.js +16 -3
- package/github/useGitHubFileReader.js.map +1 -1
- package/github/useGitHubTreeLister.d.ts +2 -1
- package/github/useGitHubTreeLister.d.ts.map +1 -1
- package/github/useGitHubTreeLister.js +7 -3
- package/github/useGitHubTreeLister.js.map +1 -1
- package/index.d.ts +3 -3
- package/index.d.ts.map +1 -1
- package/index.js +3 -3
- package/index.js.map +1 -1
- package/internal/VirtualizedThread.d.ts +3 -2
- package/internal/VirtualizedThread.d.ts.map +1 -1
- package/internal/VirtualizedThread.js +3 -2
- package/internal/VirtualizedThread.js.map +1 -1
- package/package.json +4 -4
- package/session/SessionViewer.d.ts +11 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +24 -5
- package/session/SessionViewer.js.map +1 -1
- package/session/facets/ChangesTab.d.ts +24 -6
- package/session/facets/ChangesTab.d.ts.map +1 -1
- package/session/facets/ChangesTab.js +15 -6
- package/session/facets/ChangesTab.js.map +1 -1
- package/session/index.d.ts +1 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js +1 -0
- package/session/index.js.map +1 -1
- package/session/useSessionRailViews.d.ts +16 -1
- package/session/useSessionRailViews.d.ts.map +1 -1
- package/session/useSessionRailViews.js +9 -4
- package/session/useSessionRailViews.js.map +1 -1
- package/session/useWorkspaceReadRefs.d.ts +45 -0
- package/session/useWorkspaceReadRefs.d.ts.map +1 -0
- package/session/useWorkspaceReadRefs.js +104 -0
- package/session/useWorkspaceReadRefs.js.map +1 -0
- package/src/execution/FileReviewContext.ts +27 -1
- package/src/execution/MessageThread.tsx +110 -22
- package/src/execution/TodoCard.tsx +20 -4
- package/src/execution/TodoList.tsx +26 -4
- package/src/execution/ToolCallDetail.tsx +69 -16
- package/src/execution/WriteBackCard.tsx +18 -4
- package/src/execution/__tests__/ToolCallDetail.test.tsx +188 -2
- package/src/execution/__tests__/WriteBackCard.test.tsx +83 -0
- package/src/execution/__tests__/file-review-status.test.ts +47 -0
- package/src/execution/__tests__/message-thread-slots.test.tsx +463 -0
- package/src/execution/file-review-status.ts +24 -0
- package/src/execution/index.ts +13 -3
- package/src/github/__tests__/useGitHubFileReader.test.ts +35 -2
- package/src/github/__tests__/useGitHubTreeLister.test.ts +14 -0
- package/src/github/useGitHubFileReader.ts +19 -6
- package/src/github/useGitHubTreeLister.ts +7 -3
- package/src/index.ts +4 -0
- package/src/internal/VirtualizedThread.tsx +5 -1
- package/src/session/SessionViewer.tsx +42 -2
- package/src/session/__tests__/useSessionRailViews.test.tsx +12 -0
- package/src/session/__tests__/useWorkspaceReadRefs.test.ts +245 -0
- package/src/session/facets/ChangesTab.tsx +43 -6
- package/src/session/facets/__tests__/ChangesTab.test.tsx +23 -0
- package/src/session/index.ts +2 -0
- package/src/session/useSessionRailViews.tsx +31 -3
- package/src/session/useWorkspaceReadRefs.ts +118 -0
- package/src/workflow/picker/__tests__/recents.test.ts +4 -4
- package/src/workspace/FileViewer.tsx +144 -38
- package/src/workspace/WorkspaceFileReader.ts +21 -0
- package/src/workspace/__tests__/FileViewer.test.tsx +138 -3
- package/src/workspace/__tests__/useWorkspaceFileContent.test.tsx +20 -0
- package/src/workspace/__tests__/workspaceListingCache.test.ts +114 -0
- package/src/workspace/index.ts +4 -1
- package/src/workspace/useWorkspaceEntries.ts +11 -0
- package/src/workspace/useWorkspaceFileContent.ts +4 -1
- package/src/workspace/useWorkspaceFileSearch.ts +8 -3
- package/src/workspace/useWorkspaceFiles.ts +3 -2
- package/src/workspace/workspaceListingCache.ts +21 -7
- package/styles.css +1 -1
- package/workspace/FileViewer.d.ts +1 -1
- package/workspace/FileViewer.d.ts.map +1 -1
- package/workspace/FileViewer.js +55 -5
- package/workspace/FileViewer.js.map +1 -1
- package/workspace/WorkspaceFileReader.d.ts +17 -0
- package/workspace/WorkspaceFileReader.d.ts.map +1 -1
- package/workspace/WorkspaceFileReader.js +18 -0
- package/workspace/WorkspaceFileReader.js.map +1 -1
- package/workspace/index.d.ts +1 -1
- package/workspace/index.d.ts.map +1 -1
- package/workspace/index.js +1 -1
- package/workspace/index.js.map +1 -1
- package/workspace/useWorkspaceEntries.d.ts +11 -0
- package/workspace/useWorkspaceEntries.d.ts.map +1 -1
- package/workspace/useWorkspaceEntries.js.map +1 -1
- package/workspace/useWorkspaceFileContent.d.ts.map +1 -1
- package/workspace/useWorkspaceFileContent.js +4 -1
- package/workspace/useWorkspaceFileContent.js.map +1 -1
- package/workspace/useWorkspaceFileSearch.d.ts.map +1 -1
- package/workspace/useWorkspaceFileSearch.js +8 -3
- package/workspace/useWorkspaceFileSearch.js.map +1 -1
- package/workspace/useWorkspaceFiles.d.ts +2 -1
- package/workspace/useWorkspaceFiles.d.ts.map +1 -1
- package/workspace/useWorkspaceFiles.js +3 -2
- package/workspace/useWorkspaceFiles.js.map +1 -1
- package/workspace/workspaceListingCache.d.ts +1 -1
- package/workspace/workspaceListingCache.d.ts.map +1 -1
- package/workspace/workspaceListingCache.js +20 -7
- package/workspace/workspaceListingCache.js.map +1 -1
|
@@ -38,7 +38,8 @@ const TRUNCATION_MARKER: WorkspaceFileEntry = {
|
|
|
38
38
|
* Creates a {@link WorkspaceFileLister} backed by the GitHub Trees API.
|
|
39
39
|
*
|
|
40
40
|
* Uses the existing client-side OAuth token (same one that powers
|
|
41
|
-
* `GitHubRepoPicker`) to call `GET /repos/{owner}/{repo}/git/trees/{
|
|
41
|
+
* `GitHubRepoPicker`) to call `GET /repos/{owner}/{repo}/git/trees/{ref}?recursive=1`,
|
|
42
|
+
* where the ref is the entry's `readRef` (write-back commit) or `gitBranch`.
|
|
42
43
|
*
|
|
43
44
|
* Returns `null` for:
|
|
44
45
|
* - Non-git workspace entries
|
|
@@ -64,8 +65,11 @@ export function useGitHubTreeLister(
|
|
|
64
65
|
const parsed = parseGitUrl(entry.gitUrl);
|
|
65
66
|
if (!parsed) return null;
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
// readRef (the session's write-back commit SHA, when one exists) wins
|
|
69
|
+
// over the configured branch, so the tree includes agent-created files.
|
|
70
|
+
// The Trees API accepts a branch name or a commit SHA interchangeably.
|
|
71
|
+
const ref = entry.readRef || entry.gitBranch || "main";
|
|
72
|
+
const url = `${GITHUB_TREES_API}/${parsed.owner}/${parsed.repo}/git/trees/${encodeURIComponent(ref)}?recursive=1`;
|
|
69
73
|
|
|
70
74
|
const resp = await fetch(url, {
|
|
71
75
|
headers: { Authorization: `Bearer ${token}` },
|
package/src/index.ts
CHANGED
|
@@ -71,6 +71,7 @@ export {
|
|
|
71
71
|
useWorkspaceContentSearch,
|
|
72
72
|
useWorkspaceSources,
|
|
73
73
|
MAX_WORKSPACE_FILE_READ_BYTES,
|
|
74
|
+
WorkspaceFileNotFoundError,
|
|
74
75
|
WorkspaceEditor,
|
|
75
76
|
WorkspaceFileSearch,
|
|
76
77
|
WorkspaceContentSearch,
|
|
@@ -131,6 +132,7 @@ export {
|
|
|
131
132
|
useSessionArtifacts,
|
|
132
133
|
artifactKey,
|
|
133
134
|
useSessionWriteBacks,
|
|
135
|
+
useWorkspaceReadRefs,
|
|
134
136
|
useSessionFileChanges,
|
|
135
137
|
useSessionUsage,
|
|
136
138
|
useAgentRefFromSession,
|
|
@@ -233,6 +235,7 @@ export {
|
|
|
233
235
|
deriveEffectiveVerdicts,
|
|
234
236
|
changeForRowPath,
|
|
235
237
|
fileReviewRowState,
|
|
238
|
+
fileReviewRowChange,
|
|
236
239
|
foldFileReviewEventStream,
|
|
237
240
|
displayFileChangeSets,
|
|
238
241
|
toDisplayFileChange,
|
|
@@ -290,6 +293,7 @@ export {
|
|
|
290
293
|
useApproval,
|
|
291
294
|
FileReviewContext,
|
|
292
295
|
useFileReviewRowState,
|
|
296
|
+
useFileReviewRowChange,
|
|
293
297
|
ArtifactRow,
|
|
294
298
|
ArtifactContentRenderer,
|
|
295
299
|
ArtifactContentBody,
|
|
@@ -15,6 +15,7 @@ import { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecutio
|
|
|
15
15
|
import {
|
|
16
16
|
ThreadItemRenderer,
|
|
17
17
|
threadContentColumnClass,
|
|
18
|
+
type MessageThreadSlots,
|
|
18
19
|
type ThreadItem,
|
|
19
20
|
type ThreadContentColumn,
|
|
20
21
|
} from "../execution/MessageThread.js";
|
|
@@ -55,6 +56,7 @@ export interface VirtualizedThreadProps {
|
|
|
55
56
|
readonly onRetrySend?: () => void;
|
|
56
57
|
readonly onRetryExecution?: (message: string) => void;
|
|
57
58
|
readonly onEditMessage?: (text: string) => void;
|
|
59
|
+
readonly slots?: MessageThreadSlots;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
// ---------------------------------------------------------------------------
|
|
@@ -124,6 +126,7 @@ export function VirtualizedThread({
|
|
|
124
126
|
onRetrySend,
|
|
125
127
|
onRetryExecution,
|
|
126
128
|
onEditMessage,
|
|
129
|
+
slots,
|
|
127
130
|
}: VirtualizedThreadProps) {
|
|
128
131
|
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
|
129
132
|
const scrollerRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -161,8 +164,9 @@ export function VirtualizedThread({
|
|
|
161
164
|
onRetrySend,
|
|
162
165
|
onRetryExecution,
|
|
163
166
|
onEditMessage,
|
|
167
|
+
slots,
|
|
164
168
|
}),
|
|
165
|
-
[formatToolCallSummary, onApprovalSubmit, submittingApprovalIds, approvalErrors, onBuildFromPlan, onOpenPlan, org, planActionsDisabled, planBuildPending, onRetrySend, onRetryExecution, onEditMessage],
|
|
169
|
+
[formatToolCallSummary, onApprovalSubmit, submittingApprovalIds, approvalErrors, onBuildFromPlan, onOpenPlan, org, planActionsDisabled, planBuildPending, onRetrySend, onRetryExecution, onEditMessage, slots],
|
|
166
170
|
);
|
|
167
171
|
|
|
168
172
|
return (
|
|
@@ -24,7 +24,8 @@ import { SelectionStore } from "../internal/store/selection-store.js";
|
|
|
24
24
|
import { useWorkspaceEditors, isVirtualEntryId } from "../internal/store/index.js";
|
|
25
25
|
import { ThreadSelectionContext } from "../execution/ThreadSelectionContext.js";
|
|
26
26
|
import { useSelectedThreadItem } from "../execution/useThreadSelection.js";
|
|
27
|
-
import {
|
|
27
|
+
import { isTerminalPhase } from "../execution/execution-phases.js";
|
|
28
|
+
import { MessageThread, type MessageThreadSlots } from "../execution/MessageThread.js";
|
|
28
29
|
import { FileChangeProgressBar } from "../execution/FileChangeProgressBar.js";
|
|
29
30
|
import { FileReviewDock } from "../execution/FileReviewDock.js";
|
|
30
31
|
import {
|
|
@@ -53,6 +54,7 @@ import { useSessionRailViews } from "./useSessionRailViews.js";
|
|
|
53
54
|
import { useSessionPanel, type SessionPanelController } from "./useSessionPanel.js";
|
|
54
55
|
import { SessionPanelChip } from "./SessionPanelChip.js";
|
|
55
56
|
import { useSessionWriteBacks } from "./useSessionWriteBacks.js";
|
|
57
|
+
import { useWorkspaceReadRefs } from "./useWorkspaceReadRefs.js";
|
|
56
58
|
import {
|
|
57
59
|
useSessionArtifacts,
|
|
58
60
|
artifactKey,
|
|
@@ -228,6 +230,15 @@ export interface SessionViewerProps {
|
|
|
228
230
|
readonly accessSlot?: ReactNode;
|
|
229
231
|
/** Called after a resource is applied from the Artifacts tab. */
|
|
230
232
|
readonly onApplied?: (result: ApplyResourceResult) => void;
|
|
233
|
+
/**
|
|
234
|
+
* Component overrides for the conversation thread's chrome (message
|
|
235
|
+
* bubbles, approval card, to-dos card, plan cards, thinking indicator) —
|
|
236
|
+
* forwarded to the inner {@link MessageThread}. See
|
|
237
|
+
* {@link MessageThreadSlots}. Define the object and its components at
|
|
238
|
+
* module level (or memoize) so thread rows keep their streaming
|
|
239
|
+
* re-render guarantees.
|
|
240
|
+
*/
|
|
241
|
+
readonly threadSlots?: MessageThreadSlots;
|
|
231
242
|
/** Additional CSS classes for the root container. */
|
|
232
243
|
readonly className?: string;
|
|
233
244
|
}
|
|
@@ -289,6 +300,7 @@ export function SessionViewer({
|
|
|
289
300
|
headerActions,
|
|
290
301
|
accessSlot,
|
|
291
302
|
onApplied,
|
|
303
|
+
threadSlots,
|
|
292
304
|
className,
|
|
293
305
|
}: SessionViewerProps) {
|
|
294
306
|
const flow = useSessionPageFlow({ sessionId, org, getRuntimeEnv });
|
|
@@ -572,6 +584,7 @@ export function SessionViewer({
|
|
|
572
584
|
onDismissPlanAttachFailed={() => setPlanAttachFailed(false)}
|
|
573
585
|
onFilePathClick={handleTranscriptFilePathClick}
|
|
574
586
|
isEndUser={isEndUser}
|
|
587
|
+
threadSlots={threadSlots}
|
|
575
588
|
/>
|
|
576
589
|
}
|
|
577
590
|
secondary={
|
|
@@ -637,6 +650,7 @@ interface ConversationColumnProps {
|
|
|
637
650
|
*/
|
|
638
651
|
readonly onFilePathClick: (path: string) => boolean;
|
|
639
652
|
readonly isEndUser: boolean;
|
|
653
|
+
readonly threadSlots?: MessageThreadSlots;
|
|
640
654
|
}
|
|
641
655
|
|
|
642
656
|
const ConversationColumn = memo(function ConversationColumn({
|
|
@@ -658,6 +672,7 @@ const ConversationColumn = memo(function ConversationColumn({
|
|
|
658
672
|
onDismissPlanAttachFailed,
|
|
659
673
|
onFilePathClick,
|
|
660
674
|
isEndUser,
|
|
675
|
+
threadSlots,
|
|
661
676
|
}: ConversationColumnProps) {
|
|
662
677
|
const { conv } = flow;
|
|
663
678
|
// Approval failures are NOT folded into this banner: they now surface in-card,
|
|
@@ -770,6 +785,7 @@ const ConversationColumn = memo(function ConversationColumn({
|
|
|
770
785
|
planActionsDisabled={!conv.canSendFollowUp || isBuildingFromPlan}
|
|
771
786
|
planBuildPending={isBuildingFromPlan}
|
|
772
787
|
contentColumn="center"
|
|
788
|
+
slots={threadSlots}
|
|
773
789
|
className="flex-1"
|
|
774
790
|
/>
|
|
775
791
|
<div className={CONVERSATION_COLUMN_CLASS}>
|
|
@@ -1111,6 +1127,18 @@ function SessionPanelRegion({
|
|
|
1111
1127
|
],
|
|
1112
1128
|
);
|
|
1113
1129
|
|
|
1130
|
+
// Cloud sessions with a git workspace push approved changes back to a
|
|
1131
|
+
// branch/PR, so the Changes facet is offered from the start with an honest
|
|
1132
|
+
// pre-push state. Deliberately NOT for local sessions: local mode edits the
|
|
1133
|
+
// user's own working tree and never writes back (the runner configures no
|
|
1134
|
+
// push credentials there) — promising a PR would be a lie.
|
|
1135
|
+
const expectsWriteBack =
|
|
1136
|
+
flow.executionTarget === "cloud" &&
|
|
1137
|
+
flow.workspace.entries.some((entry) => entry.type === "git");
|
|
1138
|
+
const displayPhase =
|
|
1139
|
+
flow.displayExecution?.status?.phase ??
|
|
1140
|
+
ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED;
|
|
1141
|
+
|
|
1114
1142
|
// The session facets (Config / Changes / Artifacts / Usage / Inspect) as
|
|
1115
1143
|
// injected rail views — the full inspector feature set inside one panel.
|
|
1116
1144
|
const railViews = useSessionRailViews({
|
|
@@ -1123,6 +1151,8 @@ function SessionPanelRegion({
|
|
|
1123
1151
|
onOpenPlan,
|
|
1124
1152
|
onOpenArtifact,
|
|
1125
1153
|
onActivateArtifact,
|
|
1154
|
+
expectsWriteBack,
|
|
1155
|
+
changesSettled: isTerminalPhase(displayPhase),
|
|
1126
1156
|
});
|
|
1127
1157
|
|
|
1128
1158
|
// Explorer-footer folder attach (desktop only — needs the native picker).
|
|
@@ -1133,9 +1163,19 @@ function SessionPanelRegion({
|
|
|
1133
1163
|
if (path) flow.workspace.addLocalPath(path);
|
|
1134
1164
|
}, [onBrowseLocalFolder, flow.workspace.addLocalPath]);
|
|
1135
1165
|
|
|
1166
|
+
// Entries decorated with per-entry read refs (the latest write-back commit
|
|
1167
|
+
// SHA), so the surface's read-side capabilities — file viewer, tree listing,
|
|
1168
|
+
// name search — resolve at the ref the agent's changes actually live on. A
|
|
1169
|
+
// read-side projection only: `flow.workspace.entries` stays undecorated for
|
|
1170
|
+
// setup and session input.
|
|
1171
|
+
const surfaceEntries = useWorkspaceReadRefs(
|
|
1172
|
+
flow.allExecutions,
|
|
1173
|
+
flow.workspace.entries,
|
|
1174
|
+
);
|
|
1175
|
+
|
|
1136
1176
|
return (
|
|
1137
1177
|
<WorkspaceSurface
|
|
1138
|
-
entries={
|
|
1178
|
+
entries={surfaceEntries}
|
|
1139
1179
|
lister={workspaceFileLister}
|
|
1140
1180
|
reader={workspaceFileReader}
|
|
1141
1181
|
searcher={workspaceContentSearcher}
|
|
@@ -80,6 +80,18 @@ describe("useSessionRailViews", () => {
|
|
|
80
80
|
]);
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
it("offers Changes pre-push when a write-back is expected, without a zero badge", () => {
|
|
84
|
+
const { result } = renderViews({ expectsWriteBack: true });
|
|
85
|
+
const changes = result.current.find((v) => v.id === "changes");
|
|
86
|
+
expect(changes, "cloud git sessions get the facet before any push").toBeTruthy();
|
|
87
|
+
expect(changes?.badge).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("hides Changes when no write-back exists and none is expected", () => {
|
|
91
|
+
const { result } = renderViews({ expectsWriteBack: false });
|
|
92
|
+
expect(result.current.some((v) => v.id === "changes")).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
83
95
|
it("surfaces Artifacts with a count badge", () => {
|
|
84
96
|
artifactsState.hasArtifacts = true;
|
|
85
97
|
artifactsState.artifactCount = 4;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { renderHook } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import {
|
|
5
|
+
AgentExecutionSchema,
|
|
6
|
+
AgentExecutionStatusSchema,
|
|
7
|
+
type AgentExecution,
|
|
8
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
9
|
+
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
10
|
+
import {
|
|
11
|
+
WorkspaceWriteBackSchema,
|
|
12
|
+
WorkspaceWriteBackPhase,
|
|
13
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
|
|
14
|
+
import type { WorkspaceEntry } from "../../workspace/useWorkspaceEntries";
|
|
15
|
+
import { useWorkspaceReadRefs } from "../useWorkspaceReadRefs";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Fixtures
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
function gitEntry(overrides?: Partial<WorkspaceEntry>): WorkspaceEntry {
|
|
22
|
+
return {
|
|
23
|
+
id: "ws-1",
|
|
24
|
+
name: "acme/api",
|
|
25
|
+
type: "git",
|
|
26
|
+
gitUrl: "https://github.com/acme/api",
|
|
27
|
+
gitBranch: "main",
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function localEntry(): WorkspaceEntry {
|
|
33
|
+
return {
|
|
34
|
+
id: "ws-local",
|
|
35
|
+
name: "dev/project",
|
|
36
|
+
type: "local",
|
|
37
|
+
localPath: "/Users/dev/project",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function execWithWriteBacks(
|
|
42
|
+
id: string,
|
|
43
|
+
writeBacks: ReadonlyArray<{
|
|
44
|
+
entryName: string;
|
|
45
|
+
commitSha: string;
|
|
46
|
+
phase?: WorkspaceWriteBackPhase;
|
|
47
|
+
}>,
|
|
48
|
+
): AgentExecution {
|
|
49
|
+
const exec = create(AgentExecutionSchema);
|
|
50
|
+
exec.metadata = create(ApiResourceMetadataSchema, { id });
|
|
51
|
+
exec.status = create(AgentExecutionStatusSchema);
|
|
52
|
+
exec.status.workspaceWriteBacks = writeBacks.map((wb) =>
|
|
53
|
+
create(WorkspaceWriteBackSchema, {
|
|
54
|
+
workspaceEntryName: wb.entryName,
|
|
55
|
+
commitSha: wb.commitSha,
|
|
56
|
+
phase: wb.phase ?? WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_PUSHED,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
return exec;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// useWorkspaceReadRefs
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
describe("useWorkspaceReadRefs", () => {
|
|
67
|
+
it("returns the same entries reference when there are no write-backs", () => {
|
|
68
|
+
const entries = [gitEntry()];
|
|
69
|
+
const { result } = renderHook(() => useWorkspaceReadRefs([], entries));
|
|
70
|
+
expect(result.current).toBe(entries);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("decorates a single git entry from a write-back with a matching name", () => {
|
|
74
|
+
const entries = [gitEntry({ name: "acme/api" })];
|
|
75
|
+
const executions = [
|
|
76
|
+
execWithWriteBacks("e1", [{ entryName: "acme/api", commitSha: "sha-1" }]),
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const { result } = renderHook(() =>
|
|
80
|
+
useWorkspaceReadRefs(executions, entries),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(result.current[0].readRef).toBe("sha-1");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("applies an empty-name write-back to the lone git entry (single-entry convention)", () => {
|
|
87
|
+
// Single-entry sessions provision with entryName "" — the runner's
|
|
88
|
+
// WriteBackCoordinator writes back under that empty name.
|
|
89
|
+
const entries = [gitEntry()];
|
|
90
|
+
const executions = [
|
|
91
|
+
execWithWriteBacks("e1", [{ entryName: "", commitSha: "sha-solo" }]),
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const { result } = renderHook(() =>
|
|
95
|
+
useWorkspaceReadRefs(executions, entries),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(result.current[0].readRef).toBe("sha-solo");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("claims a lone mismatched name for a lone git entry, but never for multiple entries", () => {
|
|
102
|
+
const solo = renderHook(() =>
|
|
103
|
+
useWorkspaceReadRefs(
|
|
104
|
+
[execWithWriteBacks("e1", [{ entryName: "other-name", commitSha: "sha-x" }])],
|
|
105
|
+
[gitEntry()],
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
expect(solo.result.current[0].readRef).toBe("sha-x");
|
|
109
|
+
|
|
110
|
+
// With two git entries an unmatched name is ambiguous — decorate nothing.
|
|
111
|
+
const multi = renderHook(() =>
|
|
112
|
+
useWorkspaceReadRefs(
|
|
113
|
+
[execWithWriteBacks("e1", [{ entryName: "other-name", commitSha: "sha-x" }])],
|
|
114
|
+
[
|
|
115
|
+
gitEntry({ id: "ws-1", name: "acme/api" }),
|
|
116
|
+
gitEntry({ id: "ws-2", name: "acme/web" }),
|
|
117
|
+
],
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
expect(multi.result.current[0].readRef).toBeUndefined();
|
|
121
|
+
expect(multi.result.current[1].readRef).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("the latest non-empty SHA wins across executions in order", () => {
|
|
125
|
+
const entries = [gitEntry({ name: "acme/api" })];
|
|
126
|
+
const executions = [
|
|
127
|
+
execWithWriteBacks("e1", [{ entryName: "acme/api", commitSha: "sha-old" }]),
|
|
128
|
+
execWithWriteBacks("e2", [{ entryName: "acme/api", commitSha: "sha-new" }]),
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const { result } = renderHook(() =>
|
|
132
|
+
useWorkspaceReadRefs(executions, entries),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(result.current[0].readRef).toBe("sha-new");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("a FAILED write-back with no SHA never regresses an earlier successful ref", () => {
|
|
139
|
+
const entries = [gitEntry({ name: "acme/api" })];
|
|
140
|
+
const executions = [
|
|
141
|
+
execWithWriteBacks("e1", [{ entryName: "acme/api", commitSha: "sha-good" }]),
|
|
142
|
+
execWithWriteBacks("e2", [
|
|
143
|
+
{
|
|
144
|
+
entryName: "acme/api",
|
|
145
|
+
commitSha: "",
|
|
146
|
+
phase: WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_FAILED,
|
|
147
|
+
},
|
|
148
|
+
]),
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const { result } = renderHook(() =>
|
|
152
|
+
useWorkspaceReadRefs(executions, entries),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(result.current[0].readRef).toBe("sha-good");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("returns entries unchanged when only FAILED no-SHA write-backs exist", () => {
|
|
159
|
+
const entries = [gitEntry()];
|
|
160
|
+
const executions = [
|
|
161
|
+
execWithWriteBacks("e1", [
|
|
162
|
+
{
|
|
163
|
+
entryName: "",
|
|
164
|
+
commitSha: "",
|
|
165
|
+
phase: WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_FAILED,
|
|
166
|
+
},
|
|
167
|
+
]),
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
const { result } = renderHook(() =>
|
|
171
|
+
useWorkspaceReadRefs(executions, entries),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(result.current).toBe(entries);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("matches strictly by name in multi-entry sessions", () => {
|
|
178
|
+
const entries = [
|
|
179
|
+
gitEntry({ id: "ws-1", name: "frontend" }),
|
|
180
|
+
gitEntry({ id: "ws-2", name: "backend" }),
|
|
181
|
+
];
|
|
182
|
+
const executions = [
|
|
183
|
+
execWithWriteBacks("e1", [
|
|
184
|
+
{ entryName: "frontend", commitSha: "sha-fe" },
|
|
185
|
+
{ entryName: "backend", commitSha: "sha-be" },
|
|
186
|
+
]),
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
const { result } = renderHook(() =>
|
|
190
|
+
useWorkspaceReadRefs(executions, entries),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(result.current[0].readRef).toBe("sha-fe");
|
|
194
|
+
expect(result.current[1].readRef).toBe("sha-be");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("never decorates local entries", () => {
|
|
198
|
+
const entries = [localEntry()];
|
|
199
|
+
const executions = [
|
|
200
|
+
execWithWriteBacks("e1", [{ entryName: "", commitSha: "sha-1" }]),
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
const { result } = renderHook(() =>
|
|
204
|
+
useWorkspaceReadRefs(executions, entries),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(result.current[0].readRef).toBeUndefined();
|
|
208
|
+
expect(result.current).toBe(entries);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("returns a stable array across streaming frames that change no SHA (DD-010)", () => {
|
|
212
|
+
const entries = [gitEntry({ name: "acme/api" })];
|
|
213
|
+
const { result, rerender } = renderHook(
|
|
214
|
+
(props: { executions: readonly AgentExecution[] }) =>
|
|
215
|
+
useWorkspaceReadRefs(props.executions, entries),
|
|
216
|
+
{
|
|
217
|
+
initialProps: {
|
|
218
|
+
executions: [
|
|
219
|
+
execWithWriteBacks("e1", [{ entryName: "acme/api", commitSha: "sha-1" }]),
|
|
220
|
+
] as readonly AgentExecution[],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const first = result.current;
|
|
226
|
+
expect(first[0].readRef).toBe("sha-1");
|
|
227
|
+
|
|
228
|
+
// A new executions array (fresh identity, e.g. a streaming frame) with the
|
|
229
|
+
// same SHA must return the identical decorated array.
|
|
230
|
+
rerender({
|
|
231
|
+
executions: [
|
|
232
|
+
execWithWriteBacks("e1", [{ entryName: "acme/api", commitSha: "sha-1" }]),
|
|
233
|
+
],
|
|
234
|
+
});
|
|
235
|
+
expect(result.current).toBe(first);
|
|
236
|
+
|
|
237
|
+
// Advancing the SHA produces a new decoration.
|
|
238
|
+
rerender({
|
|
239
|
+
executions: [
|
|
240
|
+
execWithWriteBacks("e1", [{ entryName: "acme/api", commitSha: "sha-2" }]),
|
|
241
|
+
],
|
|
242
|
+
});
|
|
243
|
+
expect(result.current[0].readRef).toBe("sha-2");
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -6,20 +6,42 @@ import { WriteBackCard } from "../../execution/WriteBackCard.js";
|
|
|
6
6
|
|
|
7
7
|
export interface ChangesTabProps {
|
|
8
8
|
readonly executions: readonly AgentExecution[];
|
|
9
|
+
/**
|
|
10
|
+
* Whether this session is expected to push its approved changes back to a
|
|
11
|
+
* git remote — a CLOUD session with at least one git workspace entry (local
|
|
12
|
+
* sessions edit the user's own working tree and never write back). Enables
|
|
13
|
+
* the pre-write-back states below, so the facet answers "where does my work
|
|
14
|
+
* go?" before any push exists. Defaults to `false`: a bare `ChangesTab`
|
|
15
|
+
* stays the pure write-back list it always was.
|
|
16
|
+
*/
|
|
17
|
+
readonly expectsWriteBack?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the session's latest execution is settled (terminal). Selects
|
|
20
|
+
* between the pre-write-back states: a live turn shows the "will be pushed
|
|
21
|
+
* here" promise, a settled one shows the honest "nothing pushed yet".
|
|
22
|
+
* Only consulted when {@link expectsWriteBack} is set and no write-back
|
|
23
|
+
* exists yet.
|
|
24
|
+
*/
|
|
25
|
+
readonly isSettled?: boolean;
|
|
9
26
|
}
|
|
10
27
|
|
|
11
28
|
/**
|
|
12
|
-
* Changes facet for the session panel — the
|
|
13
|
-
* (branch/commit/PR), one
|
|
29
|
+
* Changes facet for the session panel — the definitive "where did my work go"
|
|
30
|
+
* surface: the session's git write-backs (branch/commit/PR), one
|
|
31
|
+
* `WriteBackCard` per entry, with honest pre-push states for cloud git
|
|
32
|
+
* sessions whose write-back hasn't happened yet.
|
|
14
33
|
*
|
|
15
34
|
* Local file changes deliberately do NOT render here: they live in the
|
|
16
35
|
* transcript, where each stamped edit row shows its diff in place and the
|
|
17
36
|
* per-turn decision bar carries the review controls and file list
|
|
18
|
-
* (`FileReviewCard`).
|
|
19
|
-
*
|
|
20
|
-
* once a write-back exists (see `useSessionRailViews`).
|
|
37
|
+
* (`FileReviewCard`). This facet is exclusively the git OUTCOME surface —
|
|
38
|
+
* what was reviewed there lands here as a branch and pull request.
|
|
21
39
|
*/
|
|
22
|
-
export function ChangesTab({
|
|
40
|
+
export function ChangesTab({
|
|
41
|
+
executions,
|
|
42
|
+
expectsWriteBack = false,
|
|
43
|
+
isSettled = false,
|
|
44
|
+
}: ChangesTabProps) {
|
|
23
45
|
const { writeBacks, hasWriteBacks } = useSessionWriteBacks(executions);
|
|
24
46
|
|
|
25
47
|
if (hasWriteBacks) {
|
|
@@ -34,6 +56,21 @@ export function ChangesTab({ executions }: ChangesTabProps) {
|
|
|
34
56
|
);
|
|
35
57
|
}
|
|
36
58
|
|
|
59
|
+
if (expectsWriteBack) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex flex-col items-center justify-center px-4 py-8 text-center">
|
|
62
|
+
<p className="text-xs text-muted-foreground">
|
|
63
|
+
{isSettled
|
|
64
|
+
? "No changes have been pushed yet. When the agent changes files " +
|
|
65
|
+
"and you approve them, they are pushed to a branch and pull " +
|
|
66
|
+
"request here."
|
|
67
|
+
: "Changes stay in the session workspace while you review them. " +
|
|
68
|
+
"Approved changes are pushed to a branch and pull request here."}
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
37
74
|
return (
|
|
38
75
|
<div className="flex flex-col items-center justify-center px-4 py-8 text-center">
|
|
39
76
|
<p className="text-xs text-muted-foreground">
|
|
@@ -35,4 +35,27 @@ describe("ChangesTab", () => {
|
|
|
35
35
|
expect(screen.getByText(/no changes yet/i)).toBeTruthy();
|
|
36
36
|
expect(screen.getByText(/pull requests/i)).toBeTruthy();
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
// Pre-push states for a session expected to write back (cloud + git):
|
|
40
|
+
// live turns promise where approved work will land; a settled execution
|
|
41
|
+
// reports honestly that nothing was pushed.
|
|
42
|
+
it("renders the in-review promise while a write-back is expected and the session is live", () => {
|
|
43
|
+
render(<ChangesTab executions={[]} expectsWriteBack isSettled={false} />);
|
|
44
|
+
expect(screen.getByText(/stay in the session workspace/i)).toBeTruthy();
|
|
45
|
+
expect(screen.getByText(/pushed to a branch and pull request here/i)).toBeTruthy();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("renders the nothing-pushed state when expected but the execution settled without a push", () => {
|
|
49
|
+
render(<ChangesTab executions={[]} expectsWriteBack isSettled />);
|
|
50
|
+
expect(screen.getByText(/no changes have been pushed yet/i)).toBeTruthy();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("write-backs take precedence over the pre-push states", () => {
|
|
54
|
+
writeBacksReturn.writeBacks = [{ writeBack: { workspaceEntryName: "w1" } }];
|
|
55
|
+
writeBacksReturn.hasWriteBacks = true;
|
|
56
|
+
|
|
57
|
+
render(<ChangesTab executions={[]} expectsWriteBack />);
|
|
58
|
+
expect(screen.getByTestId("write-back-card")).toBeTruthy();
|
|
59
|
+
expect(screen.queryByText(/no changes/i)).toBeNull();
|
|
60
|
+
});
|
|
38
61
|
});
|
package/src/session/index.ts
CHANGED
|
@@ -45,6 +45,8 @@ export type {
|
|
|
45
45
|
UseSessionWriteBacksReturn,
|
|
46
46
|
} from "./useSessionWriteBacks.js";
|
|
47
47
|
|
|
48
|
+
export { useWorkspaceReadRefs } from "./useWorkspaceReadRefs.js";
|
|
49
|
+
|
|
48
50
|
export { useSessionFileChanges } from "./useSessionFileChanges.js";
|
|
49
51
|
export type { UseSessionFileChangesReturn } from "./useSessionFileChanges.js";
|
|
50
52
|
|
|
@@ -54,6 +54,21 @@ export interface UseSessionRailViewsOptions {
|
|
|
54
54
|
* @default true
|
|
55
55
|
*/
|
|
56
56
|
readonly includeExecutionFacets?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Whether the session is expected to push approved changes to a git remote
|
|
59
|
+
* (a cloud session with git workspace entries). When `true` the Changes
|
|
60
|
+
* facet is offered from the start — with an honest pre-push state — instead
|
|
61
|
+
* of only materializing once a write-back exists, so "where does my work
|
|
62
|
+
* go?" always has an answer. See {@link ChangesTabProps.expectsWriteBack}.
|
|
63
|
+
* @default false
|
|
64
|
+
*/
|
|
65
|
+
readonly expectsWriteBack?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Whether the session's latest execution is settled (terminal) — forwarded
|
|
68
|
+
* to the Changes facet's pre-push states. See {@link ChangesTabProps.isSettled}.
|
|
69
|
+
* @default false
|
|
70
|
+
*/
|
|
71
|
+
readonly changesSettled?: boolean;
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
/**
|
|
@@ -77,6 +92,8 @@ export function useSessionRailViews({
|
|
|
77
92
|
onOpenArtifact,
|
|
78
93
|
onActivateArtifact,
|
|
79
94
|
includeExecutionFacets = true,
|
|
95
|
+
expectsWriteBack = false,
|
|
96
|
+
changesSettled = false,
|
|
80
97
|
}: UseSessionRailViewsOptions): readonly SurfaceRailView[] {
|
|
81
98
|
const { hasWriteBacks, writeBackCount } = useSessionWriteBacks(allExecutions);
|
|
82
99
|
const { hasArtifacts, artifactCount } = useSessionArtifacts(allExecutions);
|
|
@@ -94,13 +111,22 @@ export function useSessionRailViews({
|
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
if (includeExecutionFacets) {
|
|
97
|
-
|
|
114
|
+
// Offered when a write-back exists OR the session is expected to
|
|
115
|
+
// produce one — the facet then carries an honest pre-push state
|
|
116
|
+
// instead of being invisible until the first push.
|
|
117
|
+
if (hasWriteBacks || expectsWriteBack) {
|
|
98
118
|
views.push({
|
|
99
119
|
id: "changes",
|
|
100
120
|
label: "Changes",
|
|
101
121
|
icon: <ChangesIcon />,
|
|
102
|
-
badge: writeBackCount,
|
|
103
|
-
content:
|
|
122
|
+
badge: writeBackCount > 0 ? writeBackCount : undefined,
|
|
123
|
+
content: (
|
|
124
|
+
<ChangesTab
|
|
125
|
+
executions={allExecutions}
|
|
126
|
+
expectsWriteBack={expectsWriteBack}
|
|
127
|
+
isSettled={changesSettled}
|
|
128
|
+
/>
|
|
129
|
+
),
|
|
104
130
|
});
|
|
105
131
|
}
|
|
106
132
|
|
|
@@ -145,6 +171,8 @@ export function useSessionRailViews({
|
|
|
145
171
|
}, [
|
|
146
172
|
sessionConfig,
|
|
147
173
|
includeExecutionFacets,
|
|
174
|
+
expectsWriteBack,
|
|
175
|
+
changesSettled,
|
|
148
176
|
hasWriteBacks,
|
|
149
177
|
writeBackCount,
|
|
150
178
|
hasArtifacts,
|