@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
@@ -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
+ });
@@ -62,7 +62,15 @@ export function resolveGitBrowseUrl(
62
62
  if (!repoPath) return null;
63
63
 
64
64
  const ref = commit || branch || "HEAD";
65
- const cleanRelPath = relPath.replace(/^\/+/, "");
65
+ let cleanRelPath = relPath.replace(/^\/+/, "");
66
+
67
+ // Guard against relPath that accidentally duplicates the org/repo
68
+ // already encoded in gitUrl (e.g. relPath = "acme/app/src/main.go"
69
+ // when repoPath is "acme/app"). Strip the redundant prefix so the
70
+ // final URL doesn't contain the repo path twice.
71
+ if (cleanRelPath.startsWith(repoPath + "/")) {
72
+ cleanRelPath = cleanRelPath.slice(repoPath.length + 1);
73
+ }
66
74
 
67
75
  return `https://github.com/${repoPath}/blob/${ref}/${cleanRelPath}`;
68
76
  }
@@ -83,9 +91,9 @@ export type ResolvedPathAction =
83
91
  * 1. **Platform paths** (`.stigmer/` prefix) → copy raw path.
84
92
  * 2. **Workspace paths** → match against workspace entries:
85
93
  * - Single entry: treat path as relative to that entry.
86
- * - Multiple entries: match first path segment against entry names.
87
- * Unmatched segments fall back to the first entry (optimistic —
88
- * single-workspace sessions are the common case).
94
+ * - Multiple entries: scan path segments for an entry name match
95
+ * (handles org-prefixed paths like `plantonhq/agent-fleet/...`).
96
+ * Unmatched paths fall back to the first entry.
89
97
  * 3. **Git source** → construct GitHub blob URL.
90
98
  * 4. **Local source** → join with absolute local path for copy.
91
99
  * 5. **Fallback** → copy the raw path.
@@ -139,8 +147,11 @@ export function resolvePathAction(
139
147
  /**
140
148
  * Matches a workspace-relative path against entries. With a single
141
149
  * entry the path is used as-is. With multiple entries the first path
142
- * segment is tested against entry names; unmatched paths fall back to
143
- * the first entry (same strategy as the CLI).
150
+ * segment is tested against entry names. When the first segment
151
+ * doesn't match, deeper segments are scanned this handles tool-call
152
+ * paths that embed an org prefix (e.g. `plantonhq/agent-fleet/...`
153
+ * where the entry name is `agent-fleet`). Unmatched paths fall back
154
+ * to the first entry.
144
155
  */
145
156
  function matchWorkspaceEntry(
146
157
  relPath: string,
@@ -150,13 +161,14 @@ function matchWorkspaceEntry(
150
161
  return { entry: entries[0], relPath };
151
162
  }
152
163
 
153
- const slashIdx = relPath.indexOf("/");
154
- const firstSegment = slashIdx >= 0 ? relPath.slice(0, slashIdx) : relPath;
164
+ const segments = relPath.split("/");
155
165
 
156
- for (const entry of entries) {
157
- if (entry.name === firstSegment) {
158
- const rest = slashIdx >= 0 ? relPath.slice(slashIdx + 1) : "";
159
- return { entry, relPath: rest };
166
+ for (let i = 0; i < segments.length; i++) {
167
+ for (const entry of entries) {
168
+ if (entry.name === segments[i]) {
169
+ const rest = segments.slice(i + 1).join("/");
170
+ return { entry, relPath: rest };
171
+ }
160
172
  }
161
173
  }
162
174
 
@@ -19,6 +19,12 @@ export type { UseExecutionArtifactsReturn } from "./useExecutionArtifacts";
19
19
  export { useArtifactContent } from "./useArtifactContent";
20
20
  export type { UseArtifactContentReturn } from "./useArtifactContent";
21
21
 
22
+ export { useWorkspaceWriteBacks } from "./useWorkspaceWriteBacks";
23
+ export type { UseWorkspaceWriteBacksReturn } from "./useWorkspaceWriteBacks";
24
+
25
+ export { WriteBackCard } from "./WriteBackCard";
26
+ export type { WriteBackCardProps } from "./WriteBackCard";
27
+
22
28
  export {
23
29
  isTextArtifact,
24
30
  isArtifactExpired,
@@ -104,6 +110,9 @@ export type { ArtifactPreviewModalProps } from "./ArtifactPreviewModal";
104
110
  export { ArtifactsWidget } from "./ArtifactsWidget";
105
111
  export type { ArtifactsWidgetProps } from "./ArtifactsWidget";
106
112
 
113
+ export { WriteBacksWidget } from "./WriteBacksWidget";
114
+ export type { WriteBacksWidgetProps } from "./WriteBacksWidget";
115
+
107
116
  export {
108
117
  resolveToolCategory,
109
118
  extractPrimaryArg,
@@ -118,6 +127,10 @@ export type { FilePathLinkProps } from "./FilePathLink";
118
127
  export { FilePathContext } from "./FilePathContext";
119
128
  export type { FilePathContextValue } from "./FilePathContext";
120
129
 
130
+ export { normalizeSandboxPaths } from "./sandbox-path-normalizer";
131
+ export { SandboxContext, useSandboxNormalize } from "./SandboxContext";
132
+ export type { SandboxContextValue } from "./SandboxContext";
133
+
121
134
  export { classifyPath, resolveGitBrowseUrl, resolvePathAction } from "./file-path-resolver";
122
135
  export type { PathClassification, ResolvedPathAction } from "./file-path-resolver";
123
136