@stigmer/react 0.0.52 → 0.0.54

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 (160) hide show
  1. package/deployment-mode.d.ts +35 -0
  2. package/deployment-mode.d.ts.map +1 -0
  3. package/deployment-mode.js +41 -0
  4. package/deployment-mode.js.map +1 -0
  5. package/execution/ApprovalCard.d.ts +8 -6
  6. package/execution/ApprovalCard.d.ts.map +1 -1
  7. package/execution/ApprovalCard.js +34 -96
  8. package/execution/ApprovalCard.js.map +1 -1
  9. package/execution/ArtifactCard.d.ts +11 -1
  10. package/execution/ArtifactCard.d.ts.map +1 -1
  11. package/execution/ArtifactCard.js +22 -3
  12. package/execution/ArtifactCard.js.map +1 -1
  13. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  14. package/execution/ArtifactPreviewModal.js +1 -1
  15. package/execution/ArtifactPreviewModal.js.map +1 -1
  16. package/execution/ArtifactsWidget.d.ts +26 -19
  17. package/execution/ArtifactsWidget.d.ts.map +1 -1
  18. package/execution/ArtifactsWidget.js +24 -26
  19. package/execution/ArtifactsWidget.js.map +1 -1
  20. package/execution/McpToolDetail.d.ts +48 -0
  21. package/execution/McpToolDetail.d.ts.map +1 -0
  22. package/execution/McpToolDetail.js +159 -0
  23. package/execution/McpToolDetail.js.map +1 -0
  24. package/execution/MessageThread.d.ts +10 -1
  25. package/execution/MessageThread.d.ts.map +1 -1
  26. package/execution/MessageThread.js +19 -17
  27. package/execution/MessageThread.js.map +1 -1
  28. package/execution/SandboxContext.d.ts +32 -0
  29. package/execution/SandboxContext.d.ts.map +1 -0
  30. package/execution/SandboxContext.js +26 -0
  31. package/execution/SandboxContext.js.map +1 -0
  32. package/execution/ToolArgsView.d.ts +41 -0
  33. package/execution/ToolArgsView.d.ts.map +1 -0
  34. package/execution/ToolArgsView.js +134 -0
  35. package/execution/ToolArgsView.js.map +1 -0
  36. package/execution/ToolCallDetail.d.ts +11 -4
  37. package/execution/ToolCallDetail.d.ts.map +1 -1
  38. package/execution/ToolCallDetail.js +32 -101
  39. package/execution/ToolCallDetail.js.map +1 -1
  40. package/execution/ToolCallGroup.d.ts.map +1 -1
  41. package/execution/ToolCallGroup.js +3 -2
  42. package/execution/ToolCallGroup.js.map +1 -1
  43. package/execution/ToolCallItem.d.ts +2 -0
  44. package/execution/ToolCallItem.d.ts.map +1 -1
  45. package/execution/ToolCallItem.js +13 -3
  46. package/execution/ToolCallItem.js.map +1 -1
  47. package/execution/WriteBackCard.d.ts +34 -0
  48. package/execution/WriteBackCard.d.ts.map +1 -0
  49. package/execution/WriteBackCard.js +75 -0
  50. package/execution/WriteBackCard.js.map +1 -0
  51. package/execution/WriteBacksWidget.d.ts +49 -0
  52. package/execution/WriteBacksWidget.d.ts.map +1 -0
  53. package/execution/WriteBacksWidget.js +44 -0
  54. package/execution/WriteBacksWidget.js.map +1 -0
  55. package/execution/__tests__/file-path-resolver.test.d.ts +2 -0
  56. package/execution/__tests__/file-path-resolver.test.d.ts.map +1 -0
  57. package/execution/__tests__/file-path-resolver.test.js +180 -0
  58. package/execution/__tests__/file-path-resolver.test.js.map +1 -0
  59. package/execution/file-path-resolver.d.ts +3 -3
  60. package/execution/file-path-resolver.d.ts.map +1 -1
  61. package/execution/file-path-resolver.js +23 -12
  62. package/execution/file-path-resolver.js.map +1 -1
  63. package/execution/index.d.ts +16 -1
  64. package/execution/index.d.ts.map +1 -1
  65. package/execution/index.js +9 -1
  66. package/execution/index.js.map +1 -1
  67. package/execution/sandbox-path-normalizer.d.ts +46 -0
  68. package/execution/sandbox-path-normalizer.d.ts.map +1 -0
  69. package/execution/sandbox-path-normalizer.js +73 -0
  70. package/execution/sandbox-path-normalizer.js.map +1 -0
  71. package/execution/tool-categories.d.ts +35 -8
  72. package/execution/tool-categories.d.ts.map +1 -1
  73. package/execution/tool-categories.js +76 -10
  74. package/execution/tool-categories.js.map +1 -1
  75. package/execution/tool-rendering-primitives.d.ts +61 -0
  76. package/execution/tool-rendering-primitives.d.ts.map +1 -0
  77. package/execution/tool-rendering-primitives.js +106 -0
  78. package/execution/tool-rendering-primitives.js.map +1 -0
  79. package/execution/useArtifactContent.d.ts +5 -1
  80. package/execution/useArtifactContent.d.ts.map +1 -1
  81. package/execution/useArtifactContent.js +6 -2
  82. package/execution/useArtifactContent.js.map +1 -1
  83. package/execution/useWorkspaceWriteBacks.d.ts +40 -0
  84. package/execution/useWorkspaceWriteBacks.d.ts.map +1 -0
  85. package/execution/useWorkspaceWriteBacks.js +41 -0
  86. package/execution/useWorkspaceWriteBacks.js.map +1 -0
  87. package/github/GitHubRepoPicker.d.ts +5 -2
  88. package/github/GitHubRepoPicker.d.ts.map +1 -1
  89. package/github/GitHubRepoPicker.js +133 -36
  90. package/github/GitHubRepoPicker.js.map +1 -1
  91. package/github/index.d.ts +1 -0
  92. package/github/index.d.ts.map +1 -1
  93. package/github/index.js +1 -0
  94. package/github/index.js.map +1 -1
  95. package/github/useGitHubSearch.d.ts +20 -0
  96. package/github/useGitHubSearch.d.ts.map +1 -0
  97. package/github/useGitHubSearch.js +127 -0
  98. package/github/useGitHubSearch.js.map +1 -0
  99. package/index.d.ts +9 -6
  100. package/index.d.ts.map +1 -1
  101. package/index.js +7 -3
  102. package/index.js.map +1 -1
  103. package/internal/CloudFeatureNotice.d.ts +19 -0
  104. package/internal/CloudFeatureNotice.d.ts.map +1 -0
  105. package/internal/CloudFeatureNotice.js +21 -0
  106. package/internal/CloudFeatureNotice.js.map +1 -0
  107. package/mcp-server/McpServerDetailView.d.ts +15 -1
  108. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  109. package/mcp-server/McpServerDetailView.js +11 -3
  110. package/mcp-server/McpServerDetailView.js.map +1 -1
  111. package/package.json +4 -4
  112. package/provider.d.ts +14 -2
  113. package/provider.d.ts.map +1 -1
  114. package/provider.js +3 -2
  115. package/provider.js.map +1 -1
  116. package/session/index.d.ts +4 -0
  117. package/session/index.d.ts.map +1 -1
  118. package/session/index.js +2 -0
  119. package/session/index.js.map +1 -1
  120. package/session/useSessionArtifacts.d.ts +73 -0
  121. package/session/useSessionArtifacts.d.ts.map +1 -0
  122. package/session/useSessionArtifacts.js +95 -0
  123. package/session/useSessionArtifacts.js.map +1 -0
  124. package/session/useSessionWriteBacks.d.ts +56 -0
  125. package/session/useSessionWriteBacks.d.ts.map +1 -0
  126. package/session/useSessionWriteBacks.js +56 -0
  127. package/session/useSessionWriteBacks.js.map +1 -0
  128. package/src/deployment-mode.ts +46 -0
  129. package/src/execution/ApprovalCard.tsx +130 -283
  130. package/src/execution/ArtifactCard.tsx +40 -0
  131. package/src/execution/ArtifactPreviewModal.tsx +2 -0
  132. package/src/execution/ArtifactsWidget.tsx +51 -43
  133. package/src/execution/McpToolDetail.tsx +283 -0
  134. package/src/execution/MessageThread.tsx +18 -0
  135. package/src/execution/SandboxContext.ts +47 -0
  136. package/src/execution/ToolArgsView.tsx +279 -0
  137. package/src/execution/ToolCallDetail.tsx +54 -220
  138. package/src/execution/ToolCallGroup.tsx +3 -2
  139. package/src/execution/ToolCallItem.tsx +21 -3
  140. package/src/execution/WriteBackCard.tsx +210 -0
  141. package/src/execution/WriteBacksWidget.tsx +82 -0
  142. package/src/execution/__tests__/file-path-resolver.test.ts +295 -0
  143. package/src/execution/file-path-resolver.ts +24 -12
  144. package/src/execution/index.ts +38 -0
  145. package/src/execution/sandbox-path-normalizer.ts +80 -0
  146. package/src/execution/tool-categories.ts +89 -9
  147. package/src/execution/tool-rendering-primitives.tsx +253 -0
  148. package/src/execution/useArtifactContent.ts +6 -1
  149. package/src/execution/useWorkspaceWriteBacks.ts +56 -0
  150. package/src/github/GitHubRepoPicker.tsx +413 -108
  151. package/src/github/index.ts +5 -0
  152. package/src/github/useGitHubSearch.ts +162 -0
  153. package/src/index.ts +27 -0
  154. package/src/internal/CloudFeatureNotice.tsx +60 -0
  155. package/src/mcp-server/McpServerDetailView.tsx +24 -2
  156. package/src/provider.tsx +18 -2
  157. package/src/session/index.ts +12 -0
  158. package/src/session/useSessionArtifacts.ts +143 -0
  159. package/src/session/useSessionWriteBacks.ts +94 -0
  160. package/styles.css +1 -1
@@ -16,6 +16,7 @@ import {
16
16
  extractPrimaryArg,
17
17
  type ToolCategory,
18
18
  } from "./tool-categories";
19
+ import { useSandboxNormalize } from "./SandboxContext";
19
20
 
20
21
  export interface ToolCallItemProps {
21
22
  readonly toolCall: ToolCall;
@@ -66,7 +67,7 @@ export function ToolCallItem({
66
67
  const duration = formatDuration(toolCall.startedAt, toolCall.completedAt);
67
68
  const isSubAgent = subAgentExecution != null;
68
69
 
69
- const categoryInfo = resolveToolCategory(toolCall.name);
70
+ const categoryInfo = resolveToolCategory(toolCall.name, toolCall.mcpServerSlug);
70
71
  const CategoryIcon = CATEGORY_ICON[categoryInfo.category];
71
72
  const primaryArg = extractPrimaryArg(toolCall);
72
73
 
@@ -74,6 +75,7 @@ export function ToolCallItem({
74
75
  ? subAgentExecution.subject || subAgentExecution.name || categoryInfo.label
75
76
  : categoryInfo.label;
76
77
 
78
+ const normalize = useSandboxNormalize();
77
79
  const approvalBadge = getApprovalBadge(toolCall);
78
80
 
79
81
  // Completed/skipped Read items are non-expandable — the clickable
@@ -144,7 +146,11 @@ export function ToolCallItem({
144
146
  );
145
147
  }
146
148
 
147
- const displaySubtitle = isSubAgent ? null : primaryArg;
149
+ const displaySubtitle = isSubAgent
150
+ ? null
151
+ : categoryInfo.category === "shell" && primaryArg
152
+ ? normalize(primaryArg)
153
+ : primaryArg;
148
154
 
149
155
  return (
150
156
  <div className={cn("border-b border-border/50 last:border-b-0", className)}>
@@ -255,7 +261,7 @@ const STATUS_COLOR: Record<ItemStatus, string> = {
255
261
  // Category-specific icons (inline SVG, SDK pattern)
256
262
  // ---------------------------------------------------------------------------
257
263
 
258
- const CATEGORY_ICON: Record<ToolCategory, () => React.JSX.Element> = {
264
+ export const CATEGORY_ICON: Record<ToolCategory, () => React.JSX.Element> = {
259
265
  shell: TerminalIcon,
260
266
  read: FileIcon,
261
267
  write: FilePenIcon,
@@ -265,6 +271,7 @@ const CATEGORY_ICON: Record<ToolCategory, () => React.JSX.Element> = {
265
271
  list: FolderIcon,
266
272
  think: BrainIcon,
267
273
  "sub-agent": BotIcon,
274
+ mcp: McpPlugIcon,
268
275
  unknown: WrenchIcon,
269
276
  };
270
277
 
@@ -355,6 +362,17 @@ function WrenchIcon() {
355
362
  );
356
363
  }
357
364
 
365
+ function McpPlugIcon() {
366
+ return (
367
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round">
368
+ <path d="M4 1.5V4" />
369
+ <path d="M8 1.5V4" />
370
+ <path d="M2.5 4H9.5V6.5C9.5 8.43 7.93 10 6 10C4.07 10 2.5 8.43 2.5 6.5V4Z" />
371
+ <path d="M6 10V11" />
372
+ </svg>
373
+ );
374
+ }
375
+
358
376
  // ---------------------------------------------------------------------------
359
377
  // Status icons
360
378
  // ---------------------------------------------------------------------------
@@ -0,0 +1,210 @@
1
+ "use client";
2
+
3
+ import type { WorkspaceWriteBack } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
4
+ import { WorkspaceWriteBackPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
5
+ import { cn } from "@stigmer/theme";
6
+
7
+ export interface WriteBackCardProps {
8
+ /** The workspace write-back outcome to render. */
9
+ readonly writeBack: WorkspaceWriteBack;
10
+ /** Additional CSS classes for the root element. */
11
+ readonly className?: string;
12
+ }
13
+
14
+ /**
15
+ * Renders a single workspace write-back outcome as a compact card.
16
+ *
17
+ * Shows the workspace entry name, branch, diff summary, phase
18
+ * indicator, and a "View PR" link when the pull request was
19
+ * successfully created. On failure, displays the error message.
20
+ *
21
+ * Themed via standard semantic tokens — no hardcoded colors, no
22
+ * Console dependencies. Embedders can theme this card through
23
+ * their Tailwind/CSS configuration.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * const { writeBacks } = useWorkspaceWriteBacks(execution);
28
+ *
29
+ * {writeBacks.map((wb) => (
30
+ * <WriteBackCard
31
+ * key={wb.workspaceEntryName}
32
+ * writeBack={wb}
33
+ * />
34
+ * ))}
35
+ * ```
36
+ *
37
+ * @see useWorkspaceWriteBacks — extracts write-back data from an execution
38
+ */
39
+ export function WriteBackCard({ writeBack, className }: WriteBackCardProps) {
40
+ const isFailed =
41
+ writeBack.phase === WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_FAILED;
42
+ const hasPR = !!writeBack.pullRequestUrl;
43
+
44
+ return (
45
+ <div
46
+ role="article"
47
+ aria-label={`Write-back: ${writeBack.workspaceEntryName}`}
48
+ className={cn(
49
+ "rounded-md border p-3",
50
+ isFailed ? "border-destructive/40" : "border-border",
51
+ className,
52
+ )}
53
+ >
54
+ {/* Header: icon + workspace name + phase badge */}
55
+ <div className="flex items-start gap-2">
56
+ <span className="mt-0.5 shrink-0 text-muted-foreground">
57
+ <GitBranchIcon />
58
+ </span>
59
+ <div className="min-w-0 flex-1">
60
+ <div className="flex items-center gap-2">
61
+ <span className="truncate text-sm font-medium text-foreground">
62
+ {writeBack.workspaceEntryName}
63
+ </span>
64
+ <PhaseBadge phase={writeBack.phase} />
65
+ </div>
66
+ {writeBack.branchName && (
67
+ <div className="mt-0.5 truncate font-mono text-xs text-muted-foreground">
68
+ {writeBack.branchName}
69
+ {writeBack.baseBranch && (
70
+ <span className="text-muted-foreground/60">
71
+ {" \u2190 "}
72
+ {writeBack.baseBranch}
73
+ </span>
74
+ )}
75
+ </div>
76
+ )}
77
+ </div>
78
+ </div>
79
+
80
+ {/* Diff summary */}
81
+ {writeBack.diffSummary && (
82
+ <div className="mt-1.5 whitespace-pre-wrap font-mono text-xs text-muted-foreground">
83
+ {writeBack.diffSummary}
84
+ </div>
85
+ )}
86
+
87
+ {/* Error message */}
88
+ {isFailed && writeBack.error && (
89
+ <div className="mt-1.5 rounded bg-destructive/10 px-2 py-1 text-xs text-destructive">
90
+ {writeBack.error}
91
+ </div>
92
+ )}
93
+
94
+ {/* PR link */}
95
+ {hasPR && (
96
+ <div className="mt-2">
97
+ <a
98
+ href={writeBack.pullRequestUrl}
99
+ target="_blank"
100
+ rel="noopener noreferrer"
101
+ className={cn(
102
+ "inline-flex items-center gap-1 text-xs font-medium text-primary transition-colors hover:text-primary/80",
103
+ FOCUS_RING_CLASSES,
104
+ )}
105
+ >
106
+ <ExternalLinkIcon />
107
+ View PR #{writeBack.pullRequestNumber}
108
+ </a>
109
+ </div>
110
+ )}
111
+ </div>
112
+ );
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Phase badge
117
+ // ---------------------------------------------------------------------------
118
+
119
+ const PHASE_CONFIG: Record<
120
+ number,
121
+ { label: string; className: string }
122
+ > = {
123
+ [WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_COMMITTED]: {
124
+ label: "Committed",
125
+ className: "bg-muted text-muted-foreground",
126
+ },
127
+ [WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_PUSHED]: {
128
+ label: "Pushed",
129
+ className: "bg-muted text-muted-foreground",
130
+ },
131
+ [WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_PR_CREATED]: {
132
+ label: "PR Created",
133
+ className: "bg-primary/10 text-primary",
134
+ },
135
+ [WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_FAILED]: {
136
+ label: "Failed",
137
+ className: "bg-destructive/10 text-destructive",
138
+ },
139
+ };
140
+
141
+ function PhaseBadge({ phase }: { phase: number }) {
142
+ const config = PHASE_CONFIG[phase];
143
+ if (!config) return null;
144
+
145
+ return (
146
+ <span
147
+ className={cn(
148
+ "inline-flex shrink-0 items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium leading-none",
149
+ config.className,
150
+ )}
151
+ >
152
+ {config.label}
153
+ </span>
154
+ );
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Shared style constants
159
+ // ---------------------------------------------------------------------------
160
+
161
+ const FOCUS_RING_CLASSES =
162
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:rounded-sm";
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Inline SVG icons
166
+ // ---------------------------------------------------------------------------
167
+
168
+ function GitBranchIcon() {
169
+ return (
170
+ <svg
171
+ width="14"
172
+ height="14"
173
+ viewBox="0 0 14 14"
174
+ fill="none"
175
+ stroke="currentColor"
176
+ strokeWidth="1.5"
177
+ strokeLinecap="round"
178
+ strokeLinejoin="round"
179
+ aria-hidden="true"
180
+ >
181
+ <circle cx="4" cy="3.5" r="1.5" />
182
+ <circle cx="4" cy="10.5" r="1.5" />
183
+ <circle cx="10" cy="5.5" r="1.5" />
184
+ <path d="M4 5V9" />
185
+ <path d="M10 7V5.5" />
186
+ <path d="M4 5C4 5 4 7 7 7C10 7 10 5.5 10 5.5" />
187
+ </svg>
188
+ );
189
+ }
190
+
191
+ function ExternalLinkIcon() {
192
+ return (
193
+ <svg
194
+ width="10"
195
+ height="10"
196
+ viewBox="0 0 12 12"
197
+ fill="none"
198
+ stroke="currentColor"
199
+ strokeWidth="1.5"
200
+ strokeLinecap="round"
201
+ strokeLinejoin="round"
202
+ className="shrink-0"
203
+ aria-hidden="true"
204
+ >
205
+ <path d="M9 3L3 9" />
206
+ <path d="M5 3H9V7" />
207
+ <path d="M9 8V10H2V3H4" />
208
+ </svg>
209
+ );
210
+ }
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
4
+ import { cn } from "@stigmer/theme";
5
+ import { useSessionWriteBacks } from "../session/useSessionWriteBacks";
6
+ import { WriteBackCard } from "./WriteBackCard";
7
+
8
+ export interface WriteBacksWidgetProps {
9
+ /**
10
+ * All executions for the current session — both completed and
11
+ * actively streaming. The widget aggregates write-backs across
12
+ * every execution, deduplicates by `workspace_entry_name` (latest
13
+ * wins), and sorts alphabetically.
14
+ *
15
+ * Renders nothing when the list is empty or no execution has
16
+ * write-backs.
17
+ */
18
+ readonly executions: readonly AgentExecution[];
19
+ /** Additional CSS classes for the root element. */
20
+ readonly className?: string;
21
+ }
22
+
23
+ /**
24
+ * Right-sidebar widget that surfaces pull requests created by the
25
+ * platform's incremental git write-back workflow.
26
+ *
27
+ * Write-backs from multiple executions are aggregated and
28
+ * deduplicated by `workspace_entry_name` (latest execution wins),
29
+ * presenting the user with a flat list of PRs — one per workspace
30
+ * entry.
31
+ *
32
+ * Returns `null` when no execution has write-backs, matching the
33
+ * conditional-render pattern of {@link ArtifactsWidget} and
34
+ * {@link ExecutionProgress}.
35
+ *
36
+ * All visual properties flow through `--stgm-*` tokens. Zero
37
+ * Console dependencies.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * const conv = useSessionConversation(sessionId, org);
42
+ *
43
+ * <WriteBacksWidget
44
+ * executions={[
45
+ * ...conv.completedExecutions,
46
+ * ...(conv.activeStreamExecution ? [conv.activeStreamExecution] : []),
47
+ * ]}
48
+ * />
49
+ * ```
50
+ *
51
+ * @see {@link WriteBackCard} — compact card per write-back
52
+ * @see {@link useSessionWriteBacks} — headless session-level write-back aggregation hook
53
+ * @see {@link useWorkspaceWriteBacks} — headless single-execution write-back extraction hook
54
+ */
55
+ export function WriteBacksWidget({
56
+ executions,
57
+ className,
58
+ }: WriteBacksWidgetProps) {
59
+ const { writeBacks, hasWriteBacks, writeBackCount } =
60
+ useSessionWriteBacks(executions);
61
+
62
+ if (!hasWriteBacks) return null;
63
+
64
+ return (
65
+ <section aria-label="Pull Requests" className={cn(className)}>
66
+ <div className="mb-2 flex items-center gap-2">
67
+ <h3 className="text-sm font-medium text-foreground">Pull Requests</h3>
68
+ <span className="inline-flex min-w-[1.25rem] items-center justify-center rounded-full bg-muted px-1.5 text-xs tabular-nums text-muted-foreground">
69
+ {writeBackCount}
70
+ </span>
71
+ </div>
72
+
73
+ <div role="list" className="space-y-2">
74
+ {writeBacks.map((entry) => (
75
+ <div key={entry.writeBack.workspaceEntryName} role="listitem">
76
+ <WriteBackCard writeBack={entry.writeBack} />
77
+ </div>
78
+ ))}
79
+ </div>
80
+ </section>
81
+ );
82
+ }
@@ -0,0 +1,295 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { create } from "@bufbuild/protobuf";
3
+ import {
4
+ WorkspaceEntrySchema,
5
+ WorkspaceSourceSchema,
6
+ GitRepoSourceSchema,
7
+ LocalPathSourceSchema,
8
+ } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
9
+ import type { WorkspaceEntry } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
10
+ import {
11
+ classifyPath,
12
+ resolveGitBrowseUrl,
13
+ resolvePathAction,
14
+ } from "../file-path-resolver";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Factory helpers
18
+ // ---------------------------------------------------------------------------
19
+
20
+ function gitEntry(name: string, url: string, branch = "main"): WorkspaceEntry {
21
+ return create(WorkspaceEntrySchema, {
22
+ name,
23
+ source: create(WorkspaceSourceSchema, {
24
+ source: {
25
+ case: "gitRepo",
26
+ value: create(GitRepoSourceSchema, { url, branch, commit: "" }),
27
+ },
28
+ }),
29
+ });
30
+ }
31
+
32
+ function localEntry(name: string, path: string): WorkspaceEntry {
33
+ return create(WorkspaceEntrySchema, {
34
+ name,
35
+ source: create(WorkspaceSourceSchema, {
36
+ source: {
37
+ case: "localPath",
38
+ value: create(LocalPathSourceSchema, { path }),
39
+ },
40
+ }),
41
+ });
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // classifyPath
46
+ // ---------------------------------------------------------------------------
47
+
48
+ describe("classifyPath", () => {
49
+ it("classifies platform paths", () => {
50
+ expect(classifyPath(".stigmer/skills/foo.md")).toEqual({
51
+ kind: "platform",
52
+ subpath: "skills/foo.md",
53
+ });
54
+ });
55
+
56
+ it("classifies bare .stigmer directory", () => {
57
+ expect(classifyPath(".stigmer")).toEqual({
58
+ kind: "platform",
59
+ subpath: "",
60
+ });
61
+ });
62
+
63
+ it("strips leading slashes from platform paths", () => {
64
+ expect(classifyPath("/.stigmer/inputs/bar")).toEqual({
65
+ kind: "platform",
66
+ subpath: "inputs/bar",
67
+ });
68
+ });
69
+
70
+ it("classifies workspace paths", () => {
71
+ expect(classifyPath("src/main.go")).toEqual({
72
+ kind: "workspace",
73
+ remainder: "src/main.go",
74
+ });
75
+ });
76
+
77
+ it("strips leading slashes from workspace paths", () => {
78
+ expect(classifyPath("/src/main.go")).toEqual({
79
+ kind: "workspace",
80
+ remainder: "src/main.go",
81
+ });
82
+ });
83
+ });
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // resolveGitBrowseUrl
87
+ // ---------------------------------------------------------------------------
88
+
89
+ describe("resolveGitBrowseUrl", () => {
90
+ it("constructs a correct GitHub blob URL", () => {
91
+ expect(
92
+ resolveGitBrowseUrl(
93
+ "https://github.com/acme/app.git",
94
+ "main",
95
+ "",
96
+ "src/index.ts",
97
+ ),
98
+ ).toBe("https://github.com/acme/app/blob/main/src/index.ts");
99
+ });
100
+
101
+ it("prefers commit over branch", () => {
102
+ expect(
103
+ resolveGitBrowseUrl(
104
+ "https://github.com/acme/app.git",
105
+ "main",
106
+ "abc123",
107
+ "README.md",
108
+ ),
109
+ ).toBe("https://github.com/acme/app/blob/abc123/README.md");
110
+ });
111
+
112
+ it("falls back to HEAD when both branch and commit are empty", () => {
113
+ expect(
114
+ resolveGitBrowseUrl(
115
+ "https://github.com/acme/app.git",
116
+ "",
117
+ "",
118
+ "README.md",
119
+ ),
120
+ ).toBe("https://github.com/acme/app/blob/HEAD/README.md");
121
+ });
122
+
123
+ it("returns null for non-GitHub hosts", () => {
124
+ expect(
125
+ resolveGitBrowseUrl(
126
+ "https://gitlab.com/acme/app.git",
127
+ "main",
128
+ "",
129
+ "src/main.go",
130
+ ),
131
+ ).toBeNull();
132
+ });
133
+
134
+ it("returns null for invalid URLs", () => {
135
+ expect(
136
+ resolveGitBrowseUrl("not-a-url", "main", "", "file.txt"),
137
+ ).toBeNull();
138
+ });
139
+
140
+ it("strips duplicate org/repo prefix from relPath", () => {
141
+ expect(
142
+ resolveGitBrowseUrl(
143
+ "https://github.com/plantonhq/agent-fleet.git",
144
+ "main",
145
+ "",
146
+ "plantonhq/agent-fleet/mcp-servers/mcp-server-planton.yaml",
147
+ ),
148
+ ).toBe(
149
+ "https://github.com/plantonhq/agent-fleet/blob/main/mcp-servers/mcp-server-planton.yaml",
150
+ );
151
+ });
152
+
153
+ it("does not strip when relPath shares a partial prefix", () => {
154
+ expect(
155
+ resolveGitBrowseUrl(
156
+ "https://github.com/acme/app.git",
157
+ "main",
158
+ "",
159
+ "acme/other-thing/file.txt",
160
+ ),
161
+ ).toBe("https://github.com/acme/app/blob/main/acme/other-thing/file.txt");
162
+ });
163
+
164
+ it("handles clone URLs without .git suffix", () => {
165
+ expect(
166
+ resolveGitBrowseUrl(
167
+ "https://github.com/acme/app",
168
+ "main",
169
+ "",
170
+ "src/lib.rs",
171
+ ),
172
+ ).toBe("https://github.com/acme/app/blob/main/src/lib.rs");
173
+ });
174
+ });
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // resolvePathAction — integration (exercises matchWorkspaceEntry internally)
178
+ // ---------------------------------------------------------------------------
179
+
180
+ describe("resolvePathAction", () => {
181
+ it("returns copy for empty path", () => {
182
+ const result = resolvePathAction("", []);
183
+ expect(result.action).toBe("copy");
184
+ });
185
+
186
+ it("returns copy for platform paths", () => {
187
+ const entries = [gitEntry("app", "https://github.com/acme/app.git")];
188
+ const result = resolvePathAction(".stigmer/skills/foo.md", entries);
189
+ expect(result.action).toBe("copy");
190
+ });
191
+
192
+ it("returns copy when no workspace entries", () => {
193
+ const result = resolvePathAction("src/main.go", []);
194
+ expect(result.action).toBe("copy");
195
+ });
196
+
197
+ describe("single git workspace entry", () => {
198
+ const entries = [
199
+ gitEntry("agent-fleet", "https://github.com/plantonhq/agent-fleet.git"),
200
+ ];
201
+
202
+ it("produces a GitHub link for a relative path", () => {
203
+ const result = resolvePathAction(
204
+ "mcp-servers/mcp-server-planton.yaml",
205
+ entries,
206
+ );
207
+ expect(result).toEqual({
208
+ action: "link",
209
+ url: "https://github.com/plantonhq/agent-fleet/blob/main/mcp-servers/mcp-server-planton.yaml",
210
+ tooltip: "Open on GitHub",
211
+ });
212
+ });
213
+ });
214
+
215
+ describe("multiple git workspace entries — first-segment match", () => {
216
+ const entries = [
217
+ gitEntry(
218
+ "mcp-server-planton",
219
+ "https://github.com/plantonhq/mcp-server-planton.git",
220
+ ),
221
+ gitEntry(
222
+ "agent-fleet",
223
+ "https://github.com/plantonhq/agent-fleet.git",
224
+ ),
225
+ ];
226
+
227
+ it("matches entry by first path segment and strips it", () => {
228
+ const result = resolvePathAction(
229
+ "agent-fleet/mcp-servers/mcp-server-planton.yaml",
230
+ entries,
231
+ );
232
+ expect(result).toEqual({
233
+ action: "link",
234
+ url: "https://github.com/plantonhq/agent-fleet/blob/main/mcp-servers/mcp-server-planton.yaml",
235
+ tooltip: "Open on GitHub",
236
+ });
237
+ });
238
+ });
239
+
240
+ describe("multiple git workspace entries — deep segment match (bug fix)", () => {
241
+ const entries = [
242
+ gitEntry(
243
+ "mcp-server-planton",
244
+ "https://github.com/plantonhq/mcp-server-planton.git",
245
+ ),
246
+ gitEntry(
247
+ "agent-fleet",
248
+ "https://github.com/plantonhq/agent-fleet.git",
249
+ ),
250
+ ];
251
+
252
+ it("matches entry by deeper segment when org prefix is present", () => {
253
+ const result = resolvePathAction(
254
+ "plantonhq/agent-fleet/mcp-servers/mcp-server-planton.yaml",
255
+ entries,
256
+ );
257
+ expect(result).toEqual({
258
+ action: "link",
259
+ url: "https://github.com/plantonhq/agent-fleet/blob/main/mcp-servers/mcp-server-planton.yaml",
260
+ tooltip: "Open on GitHub",
261
+ });
262
+ });
263
+ });
264
+
265
+ describe("fallback to first entry when no segment matches", () => {
266
+ const entries = [
267
+ gitEntry("my-app", "https://github.com/acme/my-app.git"),
268
+ gitEntry("my-lib", "https://github.com/acme/my-lib.git"),
269
+ ];
270
+
271
+ it("falls back to entries[0] with full path as relPath", () => {
272
+ const result = resolvePathAction("unknown/path/file.txt", entries);
273
+ expect(result).toEqual({
274
+ action: "link",
275
+ url: "https://github.com/acme/my-app/blob/main/unknown/path/file.txt",
276
+ tooltip: "Open on GitHub",
277
+ });
278
+ });
279
+ });
280
+
281
+ describe("local path workspace entry", () => {
282
+ const entries = [
283
+ localEntry("my-app", "/Users/dev/projects/my-app"),
284
+ ];
285
+
286
+ it("joins local path for copy action", () => {
287
+ const result = resolvePathAction("src/main.go", entries);
288
+ expect(result).toEqual({
289
+ action: "copy",
290
+ value: "/Users/dev/projects/my-app/src/main.go",
291
+ tooltip: "Copy path",
292
+ });
293
+ });
294
+ });
295
+ });