@stigmer/react 3.1.5 → 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.
@@ -24,6 +24,7 @@ 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 { isTerminalPhase } from "../execution/execution-phases.js";
27
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";
@@ -1126,6 +1127,18 @@ function SessionPanelRegion({
1126
1127
  ],
1127
1128
  );
1128
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
+
1129
1142
  // The session facets (Config / Changes / Artifacts / Usage / Inspect) as
1130
1143
  // injected rail views — the full inspector feature set inside one panel.
1131
1144
  const railViews = useSessionRailViews({
@@ -1138,6 +1151,8 @@ function SessionPanelRegion({
1138
1151
  onOpenPlan,
1139
1152
  onOpenArtifact,
1140
1153
  onActivateArtifact,
1154
+ expectsWriteBack,
1155
+ changesSettled: isTerminalPhase(displayPhase),
1141
1156
  });
1142
1157
 
1143
1158
  // Explorer-footer folder attach (desktop only — needs the native picker).
@@ -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;
@@ -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 session's git write-backs
13
- * (branch/commit/PR), one `WriteBackCard` per entry.
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`). Duplicating them in a side panel gave the same change a
19
- * third rendering with no added authority. The facet therefore only surfaces
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({ executions }: ChangesTabProps) {
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
  });
@@ -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
- if (hasWriteBacks) {
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: <ChangesTab executions={allExecutions} />,
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,
@@ -81,10 +81,10 @@ describe("TaskKindRecentsStore", () => {
81
81
 
82
82
  it("filters out invalid entries from localStorage", () => {
83
83
  const data = [
84
- { kind: "valid_kind", timestamp: Date.now() },
85
- { kind: 123, timestamp: Date.now() }, // invalid: kind is not string
86
- { timestamp: Date.now() }, // invalid: missing kind
87
- { kind: "another_valid", timestamp: Date.now() },
84
+ { kind: "valid_kind", timestamp: 2 },
85
+ { kind: 123, timestamp: 3 }, // invalid: kind is not string
86
+ { timestamp: 4 }, // invalid: missing kind
87
+ { kind: "another_valid", timestamp: 1 },
88
88
  ];
89
89
  globalThis.localStorage.setItem(TEST_KEY, JSON.stringify(data));
90
90