@stigmer/react 0.0.53 → 0.0.55

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.
Files changed (118) hide show
  1. package/execution/ArtifactCard.d.ts +11 -1
  2. package/execution/ArtifactCard.d.ts.map +1 -1
  3. package/execution/ArtifactCard.js +22 -3
  4. package/execution/ArtifactCard.js.map +1 -1
  5. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  6. package/execution/ArtifactPreviewModal.js +1 -1
  7. package/execution/ArtifactPreviewModal.js.map +1 -1
  8. package/execution/ArtifactsWidget.d.ts +26 -19
  9. package/execution/ArtifactsWidget.d.ts.map +1 -1
  10. package/execution/ArtifactsWidget.js +32 -26
  11. package/execution/ArtifactsWidget.js.map +1 -1
  12. package/execution/MessageThread.d.ts +10 -1
  13. package/execution/MessageThread.d.ts.map +1 -1
  14. package/execution/MessageThread.js +21 -17
  15. package/execution/MessageThread.js.map +1 -1
  16. package/execution/SandboxContext.d.ts +32 -0
  17. package/execution/SandboxContext.d.ts.map +1 -0
  18. package/execution/SandboxContext.js +26 -0
  19. package/execution/SandboxContext.js.map +1 -0
  20. package/execution/SetupProgress.d.ts +23 -13
  21. package/execution/SetupProgress.d.ts.map +1 -1
  22. package/execution/SetupProgress.js +18 -12
  23. package/execution/SetupProgress.js.map +1 -1
  24. package/execution/ToolArgsView.d.ts.map +1 -1
  25. package/execution/ToolArgsView.js +3 -1
  26. package/execution/ToolArgsView.js.map +1 -1
  27. package/execution/ToolCallDetail.d.ts.map +1 -1
  28. package/execution/ToolCallDetail.js +3 -1
  29. package/execution/ToolCallDetail.js.map +1 -1
  30. package/execution/ToolCallItem.d.ts.map +1 -1
  31. package/execution/ToolCallItem.js +7 -1
  32. package/execution/ToolCallItem.js.map +1 -1
  33. package/execution/WriteBackCard.d.ts +34 -0
  34. package/execution/WriteBackCard.d.ts.map +1 -0
  35. package/execution/WriteBackCard.js +75 -0
  36. package/execution/WriteBackCard.js.map +1 -0
  37. package/execution/WriteBacksWidget.d.ts +49 -0
  38. package/execution/WriteBacksWidget.d.ts.map +1 -0
  39. package/execution/WriteBacksWidget.js +44 -0
  40. package/execution/WriteBacksWidget.js.map +1 -0
  41. package/execution/__tests__/file-path-resolver.test.d.ts +2 -0
  42. package/execution/__tests__/file-path-resolver.test.d.ts.map +1 -0
  43. package/execution/__tests__/file-path-resolver.test.js +180 -0
  44. package/execution/__tests__/file-path-resolver.test.js.map +1 -0
  45. package/execution/file-path-resolver.d.ts +3 -3
  46. package/execution/file-path-resolver.d.ts.map +1 -1
  47. package/execution/file-path-resolver.js +23 -12
  48. package/execution/file-path-resolver.js.map +1 -1
  49. package/execution/index.d.ts +9 -0
  50. package/execution/index.d.ts.map +1 -1
  51. package/execution/index.js +5 -0
  52. package/execution/index.js.map +1 -1
  53. package/execution/sandbox-path-normalizer.d.ts +46 -0
  54. package/execution/sandbox-path-normalizer.d.ts.map +1 -0
  55. package/execution/sandbox-path-normalizer.js +73 -0
  56. package/execution/sandbox-path-normalizer.js.map +1 -0
  57. package/execution/useArtifactContent.d.ts +5 -1
  58. package/execution/useArtifactContent.d.ts.map +1 -1
  59. package/execution/useArtifactContent.js +6 -2
  60. package/execution/useArtifactContent.js.map +1 -1
  61. package/execution/useWorkspaceWriteBacks.d.ts +40 -0
  62. package/execution/useWorkspaceWriteBacks.d.ts.map +1 -0
  63. package/execution/useWorkspaceWriteBacks.js +41 -0
  64. package/execution/useWorkspaceWriteBacks.js.map +1 -0
  65. package/github/GitHubRepoPicker.d.ts +5 -2
  66. package/github/GitHubRepoPicker.d.ts.map +1 -1
  67. package/github/GitHubRepoPicker.js +133 -36
  68. package/github/GitHubRepoPicker.js.map +1 -1
  69. package/github/index.d.ts +1 -0
  70. package/github/index.d.ts.map +1 -1
  71. package/github/index.js +1 -0
  72. package/github/index.js.map +1 -1
  73. package/github/useGitHubSearch.d.ts +20 -0
  74. package/github/useGitHubSearch.d.ts.map +1 -0
  75. package/github/useGitHubSearch.js +127 -0
  76. package/github/useGitHubSearch.js.map +1 -0
  77. package/index.d.ts +6 -6
  78. package/index.d.ts.map +1 -1
  79. package/index.js +3 -3
  80. package/index.js.map +1 -1
  81. package/package.json +4 -4
  82. package/session/index.d.ts +4 -0
  83. package/session/index.d.ts.map +1 -1
  84. package/session/index.js +2 -0
  85. package/session/index.js.map +1 -1
  86. package/session/useSessionArtifacts.d.ts +73 -0
  87. package/session/useSessionArtifacts.d.ts.map +1 -0
  88. package/session/useSessionArtifacts.js +95 -0
  89. package/session/useSessionArtifacts.js.map +1 -0
  90. package/session/useSessionWriteBacks.d.ts +56 -0
  91. package/session/useSessionWriteBacks.d.ts.map +1 -0
  92. package/session/useSessionWriteBacks.js +56 -0
  93. package/session/useSessionWriteBacks.js.map +1 -0
  94. package/src/execution/ArtifactCard.tsx +40 -0
  95. package/src/execution/ArtifactPreviewModal.tsx +2 -0
  96. package/src/execution/ArtifactsWidget.tsx +67 -43
  97. package/src/execution/MessageThread.tsx +23 -1
  98. package/src/execution/SandboxContext.ts +47 -0
  99. package/src/execution/SetupProgress.tsx +33 -14
  100. package/src/execution/ToolArgsView.tsx +3 -1
  101. package/src/execution/ToolCallDetail.tsx +3 -1
  102. package/src/execution/ToolCallItem.tsx +7 -1
  103. package/src/execution/WriteBackCard.tsx +210 -0
  104. package/src/execution/WriteBacksWidget.tsx +82 -0
  105. package/src/execution/__tests__/file-path-resolver.test.ts +295 -0
  106. package/src/execution/file-path-resolver.ts +24 -12
  107. package/src/execution/index.ts +13 -0
  108. package/src/execution/sandbox-path-normalizer.ts +80 -0
  109. package/src/execution/useArtifactContent.ts +6 -1
  110. package/src/execution/useWorkspaceWriteBacks.ts +56 -0
  111. package/src/github/GitHubRepoPicker.tsx +413 -108
  112. package/src/github/index.ts +5 -0
  113. package/src/github/useGitHubSearch.ts +162 -0
  114. package/src/index.ts +14 -0
  115. package/src/session/index.ts +12 -0
  116. package/src/session/useSessionArtifacts.ts +143 -0
  117. package/src/session/useSessionWriteBacks.ts +94 -0
  118. package/styles.css +1 -1
@@ -13,6 +13,11 @@ export {
13
13
  type UseGitHubReposReturn,
14
14
  } from "./useGitHubRepos";
15
15
 
16
+ export {
17
+ useGitHubSearch,
18
+ type UseGitHubSearchReturn,
19
+ } from "./useGitHubSearch";
20
+
16
21
  export {
17
22
  GitHubRepoPicker,
18
23
  type GitHubRepoPickerProps,
@@ -0,0 +1,162 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import type { GitHubRepo } from "./useGitHubRepos";
5
+
6
+ const GITHUB_SEARCH_API = "https://api.github.com/search/repositories";
7
+ const DEBOUNCE_MS = 350;
8
+ const PER_PAGE = 30;
9
+
10
+ export interface UseGitHubSearchReturn {
11
+ readonly results: readonly GitHubRepo[];
12
+ readonly isSearching: boolean;
13
+ readonly error: string | null;
14
+ readonly query: string;
15
+ readonly setQuery: (query: string) => void;
16
+ readonly totalCount: number;
17
+ readonly hasMore: boolean;
18
+ readonly loadMore: () => void;
19
+ }
20
+
21
+ function parseSearchItem(r: Record<string, unknown>): GitHubRepo {
22
+ const ownerObj = r.owner as Record<string, unknown>;
23
+ return {
24
+ id: r.id as number,
25
+ fullName: r.full_name as string,
26
+ name: r.name as string,
27
+ owner: ownerObj.login as string,
28
+ ownerType:
29
+ (ownerObj.type as string) === "Organization" ? "Organization" : "User",
30
+ htmlUrl: r.html_url as string,
31
+ cloneUrl: r.clone_url as string,
32
+ defaultBranch: r.default_branch as string,
33
+ isPrivate: r.private as boolean,
34
+ updatedAt: r.updated_at as string,
35
+ };
36
+ }
37
+
38
+ async function searchRepos(
39
+ query: string,
40
+ page: number,
41
+ token: string | null,
42
+ ): Promise<{ repos: GitHubRepo[]; totalCount: number; hasMore: boolean }> {
43
+ const params = new URLSearchParams({
44
+ q: query,
45
+ sort: "stars",
46
+ order: "desc",
47
+ per_page: String(PER_PAGE),
48
+ page: String(page),
49
+ });
50
+
51
+ const headers: Record<string, string> = {
52
+ Accept: "application/vnd.github+json",
53
+ };
54
+ if (token) {
55
+ headers.Authorization = `Bearer ${token}`;
56
+ }
57
+
58
+ const resp = await fetch(`${GITHUB_SEARCH_API}?${params}`, { headers });
59
+
60
+ if (!resp.ok) {
61
+ if (resp.status === 403) {
62
+ throw new Error("GitHub search rate limit exceeded. Try again shortly.");
63
+ }
64
+ throw new Error(`GitHub search error: ${resp.status}`);
65
+ }
66
+
67
+ const data = (await resp.json()) as {
68
+ total_count: number;
69
+ items: Record<string, unknown>[];
70
+ };
71
+
72
+ const repos = data.items.map(parseSearchItem);
73
+ const totalCount = data.total_count;
74
+ const hasMore = page * PER_PAGE < totalCount;
75
+
76
+ return { repos, totalCount, hasMore };
77
+ }
78
+
79
+ /**
80
+ * Data hook that searches GitHub's public repository index.
81
+ *
82
+ * Uses the GitHub Search API (`/search/repositories`) with debounced input.
83
+ * Finds repositories across all of GitHub, not just the user's own repos.
84
+ * Works with or without an auth token (auth: 30 req/min; unauth: 10 req/min).
85
+ */
86
+ export function useGitHubSearch(
87
+ token: string | null,
88
+ ): UseGitHubSearchReturn {
89
+ const [query, setQuery] = useState("");
90
+ const [debouncedQuery, setDebouncedQuery] = useState("");
91
+ const [results, setResults] = useState<GitHubRepo[]>([]);
92
+ const [isSearching, setIsSearching] = useState(false);
93
+ const [error, setError] = useState<string | null>(null);
94
+ const [totalCount, setTotalCount] = useState(0);
95
+ const [hasMore, setHasMore] = useState(false);
96
+ const [page, setPage] = useState(1);
97
+ const debounceTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
98
+
99
+ useEffect(() => {
100
+ clearTimeout(debounceTimer.current);
101
+ if (!query.trim()) {
102
+ setDebouncedQuery("");
103
+ setResults([]);
104
+ setTotalCount(0);
105
+ setHasMore(false);
106
+ setError(null);
107
+ return;
108
+ }
109
+ debounceTimer.current = setTimeout(() => {
110
+ setDebouncedQuery(query.trim());
111
+ setPage(1);
112
+ }, DEBOUNCE_MS);
113
+ return () => clearTimeout(debounceTimer.current);
114
+ }, [query]);
115
+
116
+ useEffect(() => {
117
+ if (!debouncedQuery) return;
118
+
119
+ const cancelled = { current: false };
120
+
121
+ async function run() {
122
+ setIsSearching(true);
123
+ setError(null);
124
+ try {
125
+ const result = await searchRepos(debouncedQuery, page, token);
126
+ if (cancelled.current) return;
127
+ setResults((prev) =>
128
+ page === 1 ? result.repos : [...prev, ...result.repos],
129
+ );
130
+ setTotalCount(result.totalCount);
131
+ setHasMore(result.hasMore);
132
+ } catch (e) {
133
+ if (cancelled.current) return;
134
+ setError(e instanceof Error ? e.message : "Search failed");
135
+ } finally {
136
+ if (!cancelled.current) setIsSearching(false);
137
+ }
138
+ }
139
+
140
+ run();
141
+ return () => {
142
+ cancelled.current = true;
143
+ };
144
+ }, [debouncedQuery, page, token]);
145
+
146
+ const loadMore = useCallback(() => {
147
+ if (hasMore && !isSearching) {
148
+ setPage((prev) => prev + 1);
149
+ }
150
+ }, [hasMore, isSearching]);
151
+
152
+ return {
153
+ results,
154
+ isSearching,
155
+ error,
156
+ query,
157
+ setQuery,
158
+ totalCount,
159
+ hasMore,
160
+ loadMore,
161
+ };
162
+ }
package/src/index.ts CHANGED
@@ -52,6 +52,8 @@ export {
52
52
  useSessionList,
53
53
  useSessionExecutions,
54
54
  useSessionConversation,
55
+ useSessionArtifacts,
56
+ useSessionWriteBacks,
55
57
  useAgentRefFromSession,
56
58
  groupSessionsByTime,
57
59
  PENDING_SUBJECT,
@@ -68,6 +70,10 @@ export type {
68
70
  UseSessionExecutionsReturn,
69
71
  SendFollowUpOptions,
70
72
  UseSessionConversationReturn,
73
+ SessionArtifactEntry,
74
+ UseSessionArtifactsReturn,
75
+ SessionWriteBackEntry,
76
+ UseSessionWriteBacksReturn,
71
77
  UseAgentRefFromSessionReturn,
72
78
  SessionGroup,
73
79
  } from "./session";
@@ -100,6 +106,7 @@ export {
100
106
  ArtifactContentRenderer,
101
107
  ArtifactPreviewModal,
102
108
  ArtifactsWidget,
109
+ WriteBacksWidget,
103
110
  ToolArgsView,
104
111
  McpArgsView,
105
112
  McpMetadataRow,
@@ -112,6 +119,8 @@ export {
112
119
  SessionVariablesInput,
113
120
  useExecutionArtifacts,
114
121
  useArtifactContent,
122
+ useWorkspaceWriteBacks,
123
+ WriteBackCard,
115
124
  isTextArtifact,
116
125
  isArtifactExpired,
117
126
  formatArtifactSize,
@@ -145,6 +154,7 @@ export type {
145
154
  ArtifactRenderMode,
146
155
  ArtifactPreviewModalProps,
147
156
  ArtifactsWidgetProps,
157
+ WriteBacksWidgetProps,
148
158
  FilePathLinkProps,
149
159
  FilePathContextValue,
150
160
  PathClassification,
@@ -154,6 +164,8 @@ export type {
154
164
  SessionVariablesInputProps,
155
165
  UseExecutionArtifactsReturn,
156
166
  UseArtifactContentReturn,
167
+ UseWorkspaceWriteBacksReturn,
168
+ WriteBackCardProps,
157
169
  } from "./execution";
158
170
 
159
171
  // Execution — proto type re-exports for artifact consumers
@@ -257,6 +269,7 @@ export type {
257
269
  export {
258
270
  useGitHubConnection,
259
271
  useGitHubRepos,
272
+ useGitHubSearch,
260
273
  GitHubRepoPicker,
261
274
  GITHUB_CALLBACK_MESSAGE_TYPE,
262
275
  } from "./github";
@@ -267,6 +280,7 @@ export type {
267
280
  GitHubRepo,
268
281
  GitHubBranch,
269
282
  UseGitHubReposReturn,
283
+ UseGitHubSearchReturn,
270
284
  GitHubRepoPickerProps,
271
285
  } from "./github";
272
286
 
@@ -26,6 +26,18 @@ export type {
26
26
  UseSessionConversationReturn,
27
27
  } from "./useSessionConversation";
28
28
 
29
+ export { useSessionArtifacts } from "./useSessionArtifacts";
30
+ export type {
31
+ SessionArtifactEntry,
32
+ UseSessionArtifactsReturn,
33
+ } from "./useSessionArtifacts";
34
+
35
+ export { useSessionWriteBacks } from "./useSessionWriteBacks";
36
+ export type {
37
+ SessionWriteBackEntry,
38
+ UseSessionWriteBacksReturn,
39
+ } from "./useSessionWriteBacks";
40
+
29
41
  export { useAgentRefFromSession } from "./useAgentRefFromSession";
30
42
  export type { UseAgentRefFromSessionReturn } from "./useAgentRefFromSession";
31
43
 
@@ -0,0 +1,143 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
5
+ import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
6
+ import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
7
+ import { isTerminalPhase } from "../execution/execution-phases";
8
+
9
+ /**
10
+ * A single artifact entry enriched with the execution context needed
11
+ * for content fetching and Apply/Push gating.
12
+ *
13
+ * The `executionId` identifies which execution produced (or last
14
+ * updated) this artifact — required by {@link useArtifactContent} and
15
+ * {@link ArtifactPreviewModal}. `isTerminal` controls whether the
16
+ * Apply CTA is enabled in the preview modal.
17
+ */
18
+ export interface SessionArtifactEntry {
19
+ readonly artifact: ExecutionArtifact;
20
+ /** ID of the execution that produced this artifact version. */
21
+ readonly executionId: string;
22
+ /** Whether the producing execution is in a terminal phase. */
23
+ readonly isTerminal: boolean;
24
+ /**
25
+ * `true` when another artifact in the deduplicated list shares the
26
+ * same display `name` but has a different `sandbox_path`. Consumers
27
+ * use this to render path context for disambiguation.
28
+ */
29
+ readonly hasNameCollision: boolean;
30
+ }
31
+
32
+ export interface UseSessionArtifactsReturn {
33
+ /** Deduplicated, alphabetically-sorted artifacts from all executions. */
34
+ readonly artifacts: readonly SessionArtifactEntry[];
35
+ /** `true` when there is at least one artifact across all executions. */
36
+ readonly hasArtifacts: boolean;
37
+ /** Total number of deduplicated artifacts. */
38
+ readonly artifactCount: number;
39
+ }
40
+
41
+ /**
42
+ * Returns the dedup key for an artifact. Uses `sandbox_path` when
43
+ * available (stable filesystem identity within the session sandbox),
44
+ * falling back to `name` for older artifacts that predate the field.
45
+ */
46
+ function dedupKey(artifact: ExecutionArtifact): string {
47
+ return artifact.sandboxPath || artifact.name;
48
+ }
49
+
50
+ /**
51
+ * Pure derivation hook that aggregates artifacts across all executions
52
+ * in a session into a unified, deduplicated, alphabetically-sorted
53
+ * list — like a file explorer showing the conversation's output.
54
+ *
55
+ * Follows the same `useMemo`-based pattern as
56
+ * {@link useExecutionArtifacts}: no side effects, no data fetching.
57
+ *
58
+ * **Dedup semantics:** Artifacts are keyed by `sandbox_path` (the
59
+ * original filesystem path in the agent sandbox). When multiple
60
+ * executions produce an artifact at the same path, the latest
61
+ * execution's version wins — matching filesystem overwrite semantics.
62
+ *
63
+ * **Sorting:** Entries are sorted alphabetically by display `name`
64
+ * (case-insensitive). This matches the file-explorer mental model
65
+ * where users scan by filename, not by creation order.
66
+ *
67
+ * **Name collision detection:** When two artifacts share the same
68
+ * display `name` but differ in `sandbox_path`, both entries are
69
+ * flagged with `hasNameCollision: true` so consumers can render path
70
+ * context for disambiguation.
71
+ *
72
+ * @param executions - All executions for a session, in chronological
73
+ * order (as returned by `listBySession`). Pass both completed and
74
+ * active-stream executions.
75
+ *
76
+ * @example
77
+ * ```tsx
78
+ * const conv = useSessionConversation(sessionId, org);
79
+ * const allExecutions = [
80
+ * ...conv.completedExecutions,
81
+ * ...(conv.activeStreamExecution ? [conv.activeStreamExecution] : []),
82
+ * ];
83
+ * const { artifacts, hasArtifacts } = useSessionArtifacts(allExecutions);
84
+ * ```
85
+ *
86
+ * @see useExecutionArtifacts — single-execution artifact derivation
87
+ * @see ArtifactsWidget — styled component that renders this data
88
+ */
89
+ export function useSessionArtifacts(
90
+ executions: readonly AgentExecution[],
91
+ ): UseSessionArtifactsReturn {
92
+ return useMemo(() => {
93
+ const entryMap = new Map<string, SessionArtifactEntry>();
94
+
95
+ for (const execution of executions) {
96
+ const executionId = execution.metadata?.id ?? "";
97
+ const phase =
98
+ execution.status?.phase ??
99
+ ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED;
100
+ const terminal = isTerminalPhase(phase);
101
+
102
+ for (const artifact of execution.status?.artifacts ?? []) {
103
+ const key = dedupKey(artifact);
104
+ entryMap.set(key, {
105
+ artifact,
106
+ executionId,
107
+ isTerminal: terminal,
108
+ hasNameCollision: false,
109
+ });
110
+ }
111
+ }
112
+
113
+ const entries = Array.from(entryMap.values());
114
+
115
+ entries.sort((a, b) =>
116
+ a.artifact.name.localeCompare(b.artifact.name, undefined, {
117
+ sensitivity: "base",
118
+ }),
119
+ );
120
+
121
+ // Detect name collisions: entries that share a display name but
122
+ // have different sandbox paths need path context for disambiguation.
123
+ const nameCount = new Map<string, number>();
124
+ for (const entry of entries) {
125
+ const lower = entry.artifact.name.toLowerCase();
126
+ nameCount.set(lower, (nameCount.get(lower) ?? 0) + 1);
127
+ }
128
+
129
+ const result: SessionArtifactEntry[] = entries.map((entry) => {
130
+ const lower = entry.artifact.name.toLowerCase();
131
+ if ((nameCount.get(lower) ?? 0) > 1) {
132
+ return { ...entry, hasNameCollision: true };
133
+ }
134
+ return entry;
135
+ });
136
+
137
+ return {
138
+ artifacts: result,
139
+ hasArtifacts: result.length > 0,
140
+ artifactCount: result.length,
141
+ };
142
+ }, [executions]);
143
+ }
@@ -0,0 +1,94 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
5
+ import type { WorkspaceWriteBack } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
6
+
7
+ /**
8
+ * A single write-back entry enriched with the execution context that
9
+ * produced it.
10
+ *
11
+ * The `executionId` links the write-back to its originating execution
12
+ * for traceability in the UI (e.g., tooltip or detail view).
13
+ */
14
+ export interface SessionWriteBackEntry {
15
+ readonly writeBack: WorkspaceWriteBack;
16
+ /** ID of the execution that produced this write-back. */
17
+ readonly executionId: string;
18
+ }
19
+
20
+ export interface UseSessionWriteBacksReturn {
21
+ /** All write-backs from the session, ordered by workspace entry name. */
22
+ readonly writeBacks: readonly SessionWriteBackEntry[];
23
+ /** `true` when there is at least one write-back across all executions. */
24
+ readonly hasWriteBacks: boolean;
25
+ /** Total number of write-backs. */
26
+ readonly writeBackCount: number;
27
+ }
28
+
29
+ /**
30
+ * Pure derivation hook that aggregates workspace write-backs across all
31
+ * executions in a session into a flat, deduplicated list.
32
+ *
33
+ * Follows the same pattern as {@link useSessionArtifacts}: `useMemo`-based
34
+ * derivation, no side effects, no data fetching. Takes the same
35
+ * `executions` array input.
36
+ *
37
+ * **Dedup semantics:** Write-backs are keyed by `workspace_entry_name`.
38
+ * When multiple executions write back to the same workspace entry (e.g.,
39
+ * a follow-up execution on the same git repo), the latest execution's
40
+ * write-back wins — each execution creates its own branch/PR, and the
41
+ * most recent one is the one users care about.
42
+ *
43
+ * **Sorting:** Entries are sorted alphabetically by workspace entry name.
44
+ *
45
+ * @param executions - All executions for a session, in chronological
46
+ * order. Pass both completed and active-stream executions.
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * const conv = useSessionConversation(sessionId, org);
51
+ * const allExecutions = [
52
+ * ...conv.completedExecutions,
53
+ * ...(conv.activeStreamExecution ? [conv.activeStreamExecution] : []),
54
+ * ];
55
+ * const { writeBacks, hasWriteBacks } = useSessionWriteBacks(allExecutions);
56
+ * ```
57
+ *
58
+ * @see useWorkspaceWriteBacks — single-execution write-back derivation
59
+ * @see WriteBackCard — component that renders a single write-back
60
+ */
61
+ export function useSessionWriteBacks(
62
+ executions: readonly AgentExecution[],
63
+ ): UseSessionWriteBacksReturn {
64
+ return useMemo(() => {
65
+ const entryMap = new Map<string, SessionWriteBackEntry>();
66
+
67
+ for (const execution of executions) {
68
+ const executionId = execution.metadata?.id ?? "";
69
+
70
+ for (const wb of execution.status?.workspaceWriteBacks ?? []) {
71
+ entryMap.set(wb.workspaceEntryName, {
72
+ writeBack: wb,
73
+ executionId,
74
+ });
75
+ }
76
+ }
77
+
78
+ const entries = Array.from(entryMap.values());
79
+
80
+ entries.sort((a, b) =>
81
+ a.writeBack.workspaceEntryName.localeCompare(
82
+ b.writeBack.workspaceEntryName,
83
+ undefined,
84
+ { sensitivity: "base" },
85
+ ),
86
+ );
87
+
88
+ return {
89
+ writeBacks: entries,
90
+ hasWriteBacks: entries.length > 0,
91
+ writeBackCount: entries.length,
92
+ };
93
+ }, [executions]);
94
+ }