@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.
- package/execution/ArtifactCard.d.ts +11 -1
- package/execution/ArtifactCard.d.ts.map +1 -1
- package/execution/ArtifactCard.js +22 -3
- package/execution/ArtifactCard.js.map +1 -1
- package/execution/ArtifactPreviewModal.d.ts.map +1 -1
- package/execution/ArtifactPreviewModal.js +1 -1
- package/execution/ArtifactPreviewModal.js.map +1 -1
- package/execution/ArtifactsWidget.d.ts +26 -19
- package/execution/ArtifactsWidget.d.ts.map +1 -1
- package/execution/ArtifactsWidget.js +32 -26
- package/execution/ArtifactsWidget.js.map +1 -1
- package/execution/MessageThread.d.ts +10 -1
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +21 -17
- package/execution/MessageThread.js.map +1 -1
- package/execution/SandboxContext.d.ts +32 -0
- package/execution/SandboxContext.d.ts.map +1 -0
- package/execution/SandboxContext.js +26 -0
- package/execution/SandboxContext.js.map +1 -0
- package/execution/SetupProgress.d.ts +23 -13
- package/execution/SetupProgress.d.ts.map +1 -1
- package/execution/SetupProgress.js +18 -12
- package/execution/SetupProgress.js.map +1 -1
- package/execution/ToolArgsView.d.ts.map +1 -1
- package/execution/ToolArgsView.js +3 -1
- package/execution/ToolArgsView.js.map +1 -1
- package/execution/ToolCallDetail.d.ts.map +1 -1
- package/execution/ToolCallDetail.js +3 -1
- package/execution/ToolCallDetail.js.map +1 -1
- package/execution/ToolCallItem.d.ts.map +1 -1
- package/execution/ToolCallItem.js +7 -1
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/WriteBackCard.d.ts +34 -0
- package/execution/WriteBackCard.d.ts.map +1 -0
- package/execution/WriteBackCard.js +75 -0
- package/execution/WriteBackCard.js.map +1 -0
- package/execution/WriteBacksWidget.d.ts +49 -0
- package/execution/WriteBacksWidget.d.ts.map +1 -0
- package/execution/WriteBacksWidget.js +44 -0
- package/execution/WriteBacksWidget.js.map +1 -0
- package/execution/__tests__/file-path-resolver.test.d.ts +2 -0
- package/execution/__tests__/file-path-resolver.test.d.ts.map +1 -0
- package/execution/__tests__/file-path-resolver.test.js +180 -0
- package/execution/__tests__/file-path-resolver.test.js.map +1 -0
- package/execution/file-path-resolver.d.ts +3 -3
- package/execution/file-path-resolver.d.ts.map +1 -1
- package/execution/file-path-resolver.js +23 -12
- package/execution/file-path-resolver.js.map +1 -1
- package/execution/index.d.ts +9 -0
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +5 -0
- package/execution/index.js.map +1 -1
- package/execution/sandbox-path-normalizer.d.ts +46 -0
- package/execution/sandbox-path-normalizer.d.ts.map +1 -0
- package/execution/sandbox-path-normalizer.js +73 -0
- package/execution/sandbox-path-normalizer.js.map +1 -0
- package/execution/useArtifactContent.d.ts +5 -1
- package/execution/useArtifactContent.d.ts.map +1 -1
- package/execution/useArtifactContent.js +6 -2
- package/execution/useArtifactContent.js.map +1 -1
- package/execution/useWorkspaceWriteBacks.d.ts +40 -0
- package/execution/useWorkspaceWriteBacks.d.ts.map +1 -0
- package/execution/useWorkspaceWriteBacks.js +41 -0
- package/execution/useWorkspaceWriteBacks.js.map +1 -0
- package/github/GitHubRepoPicker.d.ts +5 -2
- package/github/GitHubRepoPicker.d.ts.map +1 -1
- package/github/GitHubRepoPicker.js +133 -36
- package/github/GitHubRepoPicker.js.map +1 -1
- package/github/index.d.ts +1 -0
- package/github/index.d.ts.map +1 -1
- package/github/index.js +1 -0
- package/github/index.js.map +1 -1
- package/github/useGitHubSearch.d.ts +20 -0
- package/github/useGitHubSearch.d.ts.map +1 -0
- package/github/useGitHubSearch.js +127 -0
- package/github/useGitHubSearch.js.map +1 -0
- package/index.d.ts +6 -6
- package/index.d.ts.map +1 -1
- package/index.js +3 -3
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/session/index.d.ts +4 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js +2 -0
- package/session/index.js.map +1 -1
- package/session/useSessionArtifacts.d.ts +73 -0
- package/session/useSessionArtifacts.d.ts.map +1 -0
- package/session/useSessionArtifacts.js +95 -0
- package/session/useSessionArtifacts.js.map +1 -0
- package/session/useSessionWriteBacks.d.ts +56 -0
- package/session/useSessionWriteBacks.d.ts.map +1 -0
- package/session/useSessionWriteBacks.js +56 -0
- package/session/useSessionWriteBacks.js.map +1 -0
- package/src/execution/ArtifactCard.tsx +40 -0
- package/src/execution/ArtifactPreviewModal.tsx +2 -0
- package/src/execution/ArtifactsWidget.tsx +67 -43
- package/src/execution/MessageThread.tsx +23 -1
- package/src/execution/SandboxContext.ts +47 -0
- package/src/execution/SetupProgress.tsx +33 -14
- package/src/execution/ToolArgsView.tsx +3 -1
- package/src/execution/ToolCallDetail.tsx +3 -1
- package/src/execution/ToolCallItem.tsx +7 -1
- package/src/execution/WriteBackCard.tsx +210 -0
- package/src/execution/WriteBacksWidget.tsx +82 -0
- package/src/execution/__tests__/file-path-resolver.test.ts +295 -0
- package/src/execution/file-path-resolver.ts +24 -12
- package/src/execution/index.ts +13 -0
- package/src/execution/sandbox-path-normalizer.ts +80 -0
- package/src/execution/useArtifactContent.ts +6 -1
- package/src/execution/useWorkspaceWriteBacks.ts +56 -0
- package/src/github/GitHubRepoPicker.tsx +413 -108
- package/src/github/index.ts +5 -0
- package/src/github/useGitHubSearch.ts +162 -0
- package/src/index.ts +14 -0
- package/src/session/index.ts +12 -0
- package/src/session/useSessionArtifacts.ts +143 -0
- package/src/session/useSessionWriteBacks.ts +94 -0
- 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
|
-
|
|
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:
|
|
87
|
-
*
|
|
88
|
-
*
|
|
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
|
|
143
|
-
*
|
|
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
|
|
154
|
-
const firstSegment = slashIdx >= 0 ? relPath.slice(0, slashIdx) : relPath;
|
|
164
|
+
const segments = relPath.split("/");
|
|
155
165
|
|
|
156
|
-
for (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
package/src/execution/index.ts
CHANGED
|
@@ -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
|
|