[UX] click shortcut in chat to go to source file in workbench
Browse files- app/components/chat/Artifact.tsx +13 -1
- app/components/workbench/Workbench.client.tsx +2 -2
- app/lib/.server/llm/prompts.ts +7 -7
- app/lib/stores/workbench.ts +4 -4
- app/utils/diff.spec.ts +11 -0
- app/utils/diff.ts +10 -1
app/components/chat/Artifact.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import type { ActionState } from '~/lib/runtime/action-runner';
|
|
| 7 |
import { workbenchStore } from '~/lib/stores/workbench';
|
| 8 |
import { classNames } from '~/utils/classNames';
|
| 9 |
import { cubicEasingFn } from '~/utils/easings';
|
|
|
|
| 10 |
|
| 11 |
const highlighterOptions = {
|
| 12 |
langs: ['shell'],
|
|
@@ -129,6 +130,14 @@ const actionVariants = {
|
|
| 129 |
visible: { opacity: 1, y: 0 },
|
| 130 |
};
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
const ActionList = memo(({ actions }: ActionListProps) => {
|
| 133 |
return (
|
| 134 |
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
|
|
@@ -169,7 +178,10 @@ const ActionList = memo(({ actions }: ActionListProps) => {
|
|
| 169 |
{type === 'file' ? (
|
| 170 |
<div>
|
| 171 |
Create{' '}
|
| 172 |
-
<code
|
|
|
|
|
|
|
|
|
|
| 173 |
{action.filePath}
|
| 174 |
</code>
|
| 175 |
</div>
|
|
|
|
| 7 |
import { workbenchStore } from '~/lib/stores/workbench';
|
| 8 |
import { classNames } from '~/utils/classNames';
|
| 9 |
import { cubicEasingFn } from '~/utils/easings';
|
| 10 |
+
import { WORK_DIR } from '~/utils/constants';
|
| 11 |
|
| 12 |
const highlighterOptions = {
|
| 13 |
langs: ['shell'],
|
|
|
|
| 130 |
visible: { opacity: 1, y: 0 },
|
| 131 |
};
|
| 132 |
|
| 133 |
+
function openArtifactInWorkbench(filePath: any) {
|
| 134 |
+
if (workbenchStore.currentView.get() !== 'code') {
|
| 135 |
+
workbenchStore.currentView.set('code');
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
workbenchStore.setSelectedFile(`${WORK_DIR}/${filePath}`);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
const ActionList = memo(({ actions }: ActionListProps) => {
|
| 142 |
return (
|
| 143 |
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
|
|
|
|
| 178 |
{type === 'file' ? (
|
| 179 |
<div>
|
| 180 |
Create{' '}
|
| 181 |
+
<code
|
| 182 |
+
className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md text-bolt-elements-item-contentAccent hover:underline cursor-pointer"
|
| 183 |
+
onClick={() => openArtifactInWorkbench(action.filePath)}
|
| 184 |
+
>
|
| 185 |
{action.filePath}
|
| 186 |
</code>
|
| 187 |
</div>
|
app/components/workbench/Workbench.client.tsx
CHANGED
|
@@ -180,8 +180,8 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
|
| 180 |
alert("GitHub token is required. Push to GitHub cancelled.");
|
| 181 |
return;
|
| 182 |
}
|
| 183 |
-
|
| 184 |
-
|
| 185 |
}}
|
| 186 |
>
|
| 187 |
<div className="i-ph:github-logo" />
|
|
|
|
| 180 |
alert("GitHub token is required. Push to GitHub cancelled.");
|
| 181 |
return;
|
| 182 |
}
|
| 183 |
+
|
| 184 |
+
workbenchStore.pushToGitHub(repoName, githubUsername, githubToken);
|
| 185 |
}}
|
| 186 |
>
|
| 187 |
<div className="i-ph:github-logo" />
|
app/lib/.server/llm/prompts.ts
CHANGED
|
@@ -39,20 +39,20 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
|
|
| 39 |
- rm: Remove files
|
| 40 |
- rmdir: Remove empty directories
|
| 41 |
- touch: Create empty file/update timestamp
|
| 42 |
-
|
| 43 |
System Information:
|
| 44 |
- hostname: Show system name
|
| 45 |
- ps: Display running processes
|
| 46 |
- pwd: Print working directory
|
| 47 |
- uptime: Show system uptime
|
| 48 |
- env: Environment variables
|
| 49 |
-
|
| 50 |
Development Tools:
|
| 51 |
- node: Execute Node.js code
|
| 52 |
- python3: Run Python scripts
|
| 53 |
- code: VSCode operations
|
| 54 |
- jq: Process JSON
|
| 55 |
-
|
| 56 |
Other Utilities:
|
| 57 |
- curl, head, sort, tail, clear, which, export, chmod, scho, hostname, kill, ln, xxd, alias, false, getconf, true, loadenv, wasm, xdg-open, command, exit, source
|
| 58 |
</system_constraints>
|
|
@@ -88,7 +88,7 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
|
|
| 88 |
Example:
|
| 89 |
|
| 90 |
<${MODIFICATIONS_TAG_NAME}>
|
| 91 |
-
<diff path="/
|
| 92 |
@@ -2,7 +2,10 @@
|
| 93 |
return a + b;
|
| 94 |
}
|
|
@@ -103,7 +103,7 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
|
|
| 103 |
+
|
| 104 |
+console.log('The End');
|
| 105 |
</diff>
|
| 106 |
-
<file path="/
|
| 107 |
// full file content here
|
| 108 |
</file>
|
| 109 |
</${MODIFICATIONS_TAG_NAME}>
|
|
@@ -124,7 +124,7 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
|
|
| 124 |
2. Create TodoList and TodoItem components
|
| 125 |
3. Implement localStorage for persistence
|
| 126 |
4. Add CRUD operations
|
| 127 |
-
|
| 128 |
Let's start now.
|
| 129 |
|
| 130 |
[Rest of response...]"
|
|
@@ -134,7 +134,7 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
|
|
| 134 |
1. Check network requests
|
| 135 |
2. Verify API endpoint format
|
| 136 |
3. Examine error handling
|
| 137 |
-
|
| 138 |
[Rest of response...]"
|
| 139 |
|
| 140 |
</chain_of_thought_instructions>
|
|
|
|
| 39 |
- rm: Remove files
|
| 40 |
- rmdir: Remove empty directories
|
| 41 |
- touch: Create empty file/update timestamp
|
| 42 |
+
|
| 43 |
System Information:
|
| 44 |
- hostname: Show system name
|
| 45 |
- ps: Display running processes
|
| 46 |
- pwd: Print working directory
|
| 47 |
- uptime: Show system uptime
|
| 48 |
- env: Environment variables
|
| 49 |
+
|
| 50 |
Development Tools:
|
| 51 |
- node: Execute Node.js code
|
| 52 |
- python3: Run Python scripts
|
| 53 |
- code: VSCode operations
|
| 54 |
- jq: Process JSON
|
| 55 |
+
|
| 56 |
Other Utilities:
|
| 57 |
- curl, head, sort, tail, clear, which, export, chmod, scho, hostname, kill, ln, xxd, alias, false, getconf, true, loadenv, wasm, xdg-open, command, exit, source
|
| 58 |
</system_constraints>
|
|
|
|
| 88 |
Example:
|
| 89 |
|
| 90 |
<${MODIFICATIONS_TAG_NAME}>
|
| 91 |
+
<diff path="${WORK_DIR}/src/main.js">
|
| 92 |
@@ -2,7 +2,10 @@
|
| 93 |
return a + b;
|
| 94 |
}
|
|
|
|
| 103 |
+
|
| 104 |
+console.log('The End');
|
| 105 |
</diff>
|
| 106 |
+
<file path="${WORK_DIR}/package.json">
|
| 107 |
// full file content here
|
| 108 |
</file>
|
| 109 |
</${MODIFICATIONS_TAG_NAME}>
|
|
|
|
| 124 |
2. Create TodoList and TodoItem components
|
| 125 |
3. Implement localStorage for persistence
|
| 126 |
4. Add CRUD operations
|
| 127 |
+
|
| 128 |
Let's start now.
|
| 129 |
|
| 130 |
[Rest of response...]"
|
|
|
|
| 134 |
1. Check network requests
|
| 135 |
2. Verify API endpoint format
|
| 136 |
3. Examine error handling
|
| 137 |
+
|
| 138 |
[Rest of response...]"
|
| 139 |
|
| 140 |
</chain_of_thought_instructions>
|
app/lib/stores/workbench.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { saveAs } from 'file-saver';
|
|
| 14 |
import { Octokit, type RestEndpointMethodTypes } from "@octokit/rest";
|
| 15 |
import * as nodePath from 'node:path';
|
| 16 |
import type { WebContainerProcess } from '@webcontainer/api';
|
|
|
|
| 17 |
|
| 18 |
export interface ArtifactState {
|
| 19 |
id: string;
|
|
@@ -312,8 +313,7 @@ export class WorkbenchStore {
|
|
| 312 |
|
| 313 |
for (const [filePath, dirent] of Object.entries(files)) {
|
| 314 |
if (dirent?.type === 'file' && !dirent.isBinary) {
|
| 315 |
-
|
| 316 |
-
const relativePath = filePath.replace(/^\/home\/project\//, '');
|
| 317 |
|
| 318 |
// split the path into segments
|
| 319 |
const pathSegments = relativePath.split('/');
|
|
@@ -343,7 +343,7 @@ export class WorkbenchStore {
|
|
| 343 |
|
| 344 |
for (const [filePath, dirent] of Object.entries(files)) {
|
| 345 |
if (dirent?.type === 'file' && !dirent.isBinary) {
|
| 346 |
-
const relativePath = filePath
|
| 347 |
const pathSegments = relativePath.split('/');
|
| 348 |
let currentHandle = targetHandle;
|
| 349 |
|
|
@@ -417,7 +417,7 @@ export class WorkbenchStore {
|
|
| 417 |
content: Buffer.from(dirent.content).toString('base64'),
|
| 418 |
encoding: 'base64',
|
| 419 |
});
|
| 420 |
-
return { path: filePath
|
| 421 |
}
|
| 422 |
})
|
| 423 |
);
|
|
|
|
| 14 |
import { Octokit, type RestEndpointMethodTypes } from "@octokit/rest";
|
| 15 |
import * as nodePath from 'node:path';
|
| 16 |
import type { WebContainerProcess } from '@webcontainer/api';
|
| 17 |
+
import { extractRelativePath } from '~/utils/diff';
|
| 18 |
|
| 19 |
export interface ArtifactState {
|
| 20 |
id: string;
|
|
|
|
| 313 |
|
| 314 |
for (const [filePath, dirent] of Object.entries(files)) {
|
| 315 |
if (dirent?.type === 'file' && !dirent.isBinary) {
|
| 316 |
+
const relativePath = extractRelativePath(filePath);
|
|
|
|
| 317 |
|
| 318 |
// split the path into segments
|
| 319 |
const pathSegments = relativePath.split('/');
|
|
|
|
| 343 |
|
| 344 |
for (const [filePath, dirent] of Object.entries(files)) {
|
| 345 |
if (dirent?.type === 'file' && !dirent.isBinary) {
|
| 346 |
+
const relativePath = extractRelativePath(filePath);
|
| 347 |
const pathSegments = relativePath.split('/');
|
| 348 |
let currentHandle = targetHandle;
|
| 349 |
|
|
|
|
| 417 |
content: Buffer.from(dirent.content).toString('base64'),
|
| 418 |
encoding: 'base64',
|
| 419 |
});
|
| 420 |
+
return { path: extractRelativePath(filePath), sha: blob.sha };
|
| 421 |
}
|
| 422 |
})
|
| 423 |
);
|
app/utils/diff.spec.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, expect, it } from 'vitest';
|
| 2 |
+
import { extractRelativePath } from './diff';
|
| 3 |
+
import { WORK_DIR } from './constants';
|
| 4 |
+
|
| 5 |
+
describe('Diff', () => {
|
| 6 |
+
it('should strip out Work_dir', () => {
|
| 7 |
+
const filePath = `${WORK_DIR}/index.js`;
|
| 8 |
+
const result = extractRelativePath(filePath);
|
| 9 |
+
expect(result).toBe('index.js');
|
| 10 |
+
});
|
| 11 |
+
});
|
app/utils/diff.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { createTwoFilesPatch } from 'diff';
|
| 2 |
import type { FileMap } from '~/lib/stores/files';
|
| 3 |
-
import { MODIFICATIONS_TAG_NAME } from './constants';
|
| 4 |
|
| 5 |
export const modificationsRegex = new RegExp(
|
| 6 |
`^<${MODIFICATIONS_TAG_NAME}>[\\s\\S]*?<\\/${MODIFICATIONS_TAG_NAME}>\\s+`,
|
|
@@ -75,6 +75,15 @@ export function diffFiles(fileName: string, oldFileContent: string, newFileConte
|
|
| 75 |
return unifiedDiff;
|
| 76 |
}
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
/**
|
| 79 |
* Converts the unified diff to HTML.
|
| 80 |
*
|
|
|
|
| 1 |
import { createTwoFilesPatch } from 'diff';
|
| 2 |
import type { FileMap } from '~/lib/stores/files';
|
| 3 |
+
import { MODIFICATIONS_TAG_NAME, WORK_DIR } from './constants';
|
| 4 |
|
| 5 |
export const modificationsRegex = new RegExp(
|
| 6 |
`^<${MODIFICATIONS_TAG_NAME}>[\\s\\S]*?<\\/${MODIFICATIONS_TAG_NAME}>\\s+`,
|
|
|
|
| 75 |
return unifiedDiff;
|
| 76 |
}
|
| 77 |
|
| 78 |
+
const regex = new RegExp(`^${WORK_DIR}\/`);
|
| 79 |
+
|
| 80 |
+
/**
|
| 81 |
+
* Strips out the work directory from the file path.
|
| 82 |
+
*/
|
| 83 |
+
export function extractRelativePath(filePath: string) {
|
| 84 |
+
return filePath.replace(regex, '');
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
/**
|
| 88 |
* Converts the unified diff to HTML.
|
| 89 |
*
|