feat: add file change summary cards to assistant messages

Display artifact changes in chat with visual indicators for create/modify/delete actions, including file counts, color-coded status badges, and scrollable file list
This commit is contained in:
Andrew Beal 2026-07-11 13:49:44 +01:00
parent cc45949eba
commit f4c3b5b826
10 changed files with 389 additions and 41 deletions

View file

@ -12,6 +12,8 @@
import { setIcon } from "obsidian";
import { fade } from "svelte/transition";
import GraphAnimation from "./GraphAnimation.svelte";
import { ArtifactAction, artifactActionToCopy } from "Enums/ArtifactAction";
import { basename } from "path-browserify";
export let messages: ConversationContent[] = [];
export let currentThought: string | null = null;
@ -109,6 +111,14 @@
};
}
function tallyArtifactsByAction(artifacts: { action: ArtifactAction }[]): Partial<Record<ArtifactAction, number>> {
const tally: Partial<Record<ArtifactAction, number>> = {};
for (const artifact of artifacts) {
tally[artifact.action] = (tally[artifact.action] ?? 0) + 1;
}
return tally;
}
$: if (scrollToBottomButton) {
setIcon(scrollToBottomButton, "arrow-down");
}
@ -158,6 +168,35 @@
<div class="markdown-content">
<div use:messageRenderAction={message} class="streaming-content"></div>
</div>
{#if message.artifacts.length > 0}
{@const artifactTally = tallyArtifactsByAction(message.artifacts)}
<div class="artifacts-container" in:fade={{ duration: 300 }}>
<span class="artifacts-container-title">{message.artifacts.length} FILES CHANGED</span>
<div class="artifacts-tally">
{#each Object.values(ArtifactAction) as action}
{#if artifactTally[action]}
<div class="artifact-tally-container">
<span class="artifact-tally-ellipse artifact-{action}"></span>
<span class="artifact-tally-count">{artifactTally[action]}</span>
</div>
{/if}
{/each}
</div>
<div class="artifacts-list-container">
{#each message.artifacts as artifact}
<div class="artifact-card" aria-label="{artifact.filePath}">
<span class="artifact-ellipse artifact-{artifact.action}"></span>
<div
class="artifact-icon"
use:setElementIcon={artifact.getIconName()}
></div>
<span class="artifact-name">{basename(artifact.filePath)}</span>
<span class="artifact-action artifact-action-{artifact.action}">{artifactActionToCopy(artifact.action)}</span>
</div>
{/each}
</div>
</div>
{/if}
</div>
</div>
{/if}
@ -270,6 +309,7 @@
}
.message-group.latest {
margin-top: var(--size-4-2);
min-height: 100%;
}
@ -316,7 +356,7 @@
.message-bubble.assistant {
word-wrap: break-word;
max-width: 100%;
width: 100%;
}
.message-text-user-container {
@ -333,7 +373,182 @@
.message-text-user-container {
padding-bottom: var(--size-4-2);
}
.artifacts-container {
display: grid;
grid-template-rows: auto auto;
grid-template-columns: auto auto;
width: 100%;
margin-top: var(--size-4-3);
gap: var(--size-4-4);
}
.artifacts-container-title {
grid-row: 1;
grid-column: 1;
font-size: var(--font-smallest);
}
.artifacts-tally {
grid-row: 1;
grid-column: 2;
display: flex;
flex-direction: row;
}
.artifact-tally-container {
display: flex;
flex-direction: row;
margin-left: auto;
align-items: center;
}
.artifact-tally-ellipse {
flex-shrink: 0;
width: 6px;
height: 6px;
border-radius: 50%;
margin-left: var(--size-4-2);
}
.artifact-create {
background: var(--color-green);
}
.artifact-modify {
background: var(--color-blue);
}
.artifact-delete {
background: var(--color-red);
}
.artifact-tally-count {
font-size: var(--font-smallest);
margin-left: var(--size-4-1);
}
.artifacts-list-container {
grid-row: 2;
grid-column: 1 / 3;
display: flex;
flex-direction: column;
overflow: scroll;
width: 100%;
max-height: 200px;
gap: var(--size-4-1);
}
.artifacts-list-container::-webkit-scrollbar {
display: none;
}
.artifact-card {
display: grid;
grid-template-rows: auto;
grid-template-columns: auto auto 1fr auto;
gap: var(--size-4-3);
align-items: center;
height: 30px;
flex-shrink: 0;
background-color: var(--background-secondary-alt);
border-radius: var(--size-4-2);
padding: 0 var(--size-4-1) 0 var(--size-4-2);
}
.artifact-ellipse {
grid-row: 1;
grid-column: 1;
flex-shrink: 0;
width: 10px;
height: 10px;
border-radius: 50%;
align-self: center;
}
.artifact-ellipse-create {
background: var(--color-green);
}
.artifact-ellipse-modify {
background: var(--color-blue);
}
.artifact-ellipse-delete {
background: var(--color-red);
}
.artifact-icon {
grid-row: 1;
grid-column: 2;
display: flex;
align-items: center;
justify-content: center;
}
.artifact-name {
grid-row: 1;
grid-column: 3;
display: flex;
align-items: center;
justify-content: start;
font-size: var(--font-smaller);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.artifact-action {
grid-row: 1;
grid-column: 4;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-smallest);
font-weight: var(--font-semibold);
border-radius: var(--size-4-2);
padding: var(--size-2-1) var(--size-4-2);
}
.artifact-action-create {
background-color: color-mix(
in srgb,
var(--color-green) 25%,
black 20%
);
color: color-mix(
in srgb,
var(--color-green) 100%,
white 10%
);
}
.artifact-action-modify {
background-color: color-mix(
in srgb,
var(--color-blue) 25%,
black 20%
);
color: color-mix(
in srgb,
var(--color-blue) 100%,
white 10%
);
}
.artifact-action-delete {
background-color: color-mix(
in srgb,
var(--color-red) 25%,
black 20%
);
color: color-mix(
in srgb,
var(--color-red) 100%,
white 10%
);
}
.conversation-empty-container {
display: grid;
height: 100%;

View file

@ -1,4 +1,6 @@
<script lang="ts">
import { ARTIFACT_ACTION_RANK, ArtifactAction } from "Enums/ArtifactAction";
import { Resolve } from "Services/DependencyService";
import { Services } from "Services/Services";
import ChatArea from "./ChatArea.svelte";
@ -21,9 +23,10 @@
import { AITool, fromString } from "Enums/AITool";
import { AIProvider } from "Enums/ApiProvider";
import type { PlanApprovalService } from "Services/PlanApprovalService";
import type { Artifact } from "Conversations/Artifact";
import { Artifact } from "Conversations/Artifact";
import { ConversationContent } from "Conversations/ConversationContent";
import { Role } from "Enums/Role";
import { basename } from "path-browserify";
const plugin: VaultkeeperAIPlugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
const executionPlanStore: ExecutionPlanStore = Resolve<ExecutionPlanStore>(Services.ExecutionPlanStore);
@ -174,6 +177,7 @@
},
onComplete: async () => {
saveCollectedArtifects(conversation);
conversation = conversation;
conversationService.saveConversation(conversation);
isSubmitting = false;
busyPlanning = false;
@ -194,7 +198,7 @@
conversation.contents.push(lastMessage);
}
lastMessage.artifacts = collectedArtifacts;
lastMessage.artifacts = Artifact.sort(collectedArtifacts);
}
$: if ($conversationStore.shouldReset) {

View file

@ -1,17 +1,31 @@
import type { IBinaryFile } from "Conversations/IBinaryFile";
import { ARTIFACT_ACTION_RANK, isArtifactAction, type ArtifactAction } from "Enums/ArtifactAction";
import { isAudioFile, isImageFile, isKnownFileType, isTextFile, isVideoFile } from "Enums/FileType";
import { pathExtname } from "Helpers/Helpers";
import { basename } from "path-browserify";
export class Artifact implements IBinaryFile {
public filePath: string;
public mimeType: string;
public action: ArtifactAction;
public originalContent: string;
public updatedContent: string;
public base64: string | undefined;
public artifactPath?: string;
public constructor(filePath: string, mimeType: string, originalContent: string, updatedContent: string, base64?: string, artifactPath?: string) {
public static sort(artifacts: Artifact[]): Artifact[] {
artifacts.sort((a, b) =>
ARTIFACT_ACTION_RANK[a.action] - ARTIFACT_ACTION_RANK[b.action] ||
basename(a.filePath).localeCompare(basename(b.filePath))
);
return artifacts;
}
public constructor(filePath: string, mimeType: string, action: ArtifactAction, originalContent: string, updatedContent: string, base64?: string, artifactPath?: string) {
this.filePath = filePath;
this.mimeType = mimeType;
this.action = action;
this.originalContent = originalContent;
this.updatedContent = updatedContent;
this.base64 = base64;
@ -26,9 +40,31 @@ export class Artifact implements IBinaryFile {
this.artifactPath = path;
}
public getIconName(): string {
const extension = pathExtname(this.filePath);
if (isTextFile(extension)) {
return "file-text";
}
if (isImageFile(extension)) {
return "file-image";
}
if (isAudioFile(extension)) {
return "file-music";
}
if (isVideoFile(extension)) {
return "file-play";
}
if (isKnownFileType(extension)) {
return "file";
}
return "file";
}
public static isArtifactData(this: void, data: unknown): data is {
filePath: string;
mimeType: string;
action: ArtifactAction;
originalContent: string;
updatedContent: string;
base64?: string;
@ -41,6 +77,9 @@ export class Artifact implements IBinaryFile {
typeof data.filePath === "string" &&
"mimeType" in data &&
typeof data.mimeType === "string" &&
"action" in data &&
typeof data.action === "string" &&
isArtifactAction(data.action) &&
"originalContent" in data &&
typeof data.originalContent === "string" &&
"updatedContent" in data &&

28
Enums/ArtifactAction.ts Normal file
View file

@ -0,0 +1,28 @@
import { Copy } from "Enums/Copy";
export enum ArtifactAction {
Create = "create",
Modify = "modify",
Delete = "delete"
}
export const ARTIFACT_ACTION_RANK = {
[ArtifactAction.Create]: 0,
[ArtifactAction.Modify]: 1,
[ArtifactAction.Delete]: 2,
};
export function isArtifactAction(value: string): value is ArtifactAction {
return Object.values(ArtifactAction).includes(value as ArtifactAction);
}
export function artifactActionToCopy(artifactAction: ArtifactAction): string {
switch (artifactAction) {
case ArtifactAction.Create:
return Copy.ArtifactActionCreated;
case ArtifactAction.Modify:
return Copy.ArtifactActionModified;
case ArtifactAction.Delete:
return Copy.ArtifactActionDeleted;
}
}

View file

@ -469,6 +469,11 @@ Each AI provider has their own data policies:
// Conversation Modal Copy
NoConversationsFound = "No conversations match your search.",
// Artifact Copy
ArtifactActionCreated = "CREATED",
ArtifactActionModified = "MODIFIED",
ArtifactActionDeleted = "DELETED",
// Help Modal Additional Copy
HelpModalCloseAriaLabel = "Close Help Modal",
PluginVersionPrefix = "Plugin version: ",

View file

@ -38,6 +38,7 @@ import {
} from "AIClasses/Schemas/AIToolSchemas";
import { Artifact } from "Conversations/Artifact";
import { extname } from "path-browserify";
import { ArtifactAction } from "Enums/ArtifactAction";
export class AIToolService {
@ -415,28 +416,6 @@ export class AIToolService {
: new AIToolResponsePayload({ path: path, success: true }, artifacts);
}
private async collectDeletionCandidatesArtifacts(filePaths: string[]): Promise<Artifact[]> {
const artifacts: Artifact[] = [];
for (const filePath of filePaths) {
const fileType = toFileType(extname(filePath));
// Anything that isn't a note we will just store as a binary artifact
if (isBinaryFile(fileType) || isDocumentMimeType(FileTypeToMimeType[fileType])) {
const result = await this.fileSystemService.readBinaryFile(filePath);
if (result instanceof ArrayBuffer) {
artifacts.push(new Artifact(filePath, FileTypeToMimeType[fileType], "", "", arrayBufferToBase64(result)));
}
} else {
const result = await this.fileSystemService.readFilePath(filePath);
if (typeof result === "string") {
artifacts.push(new Artifact(filePath, FileTypeToMimeType[fileType], result, ""));
}
}
}
return artifacts;
}
private async moveVaultFolder(sourcePath: string, destinationPath: string): Promise<AIToolResponsePayload> {
const result = await this.fileSystemService.moveFile(sourcePath, destinationPath);
if (result instanceof Error) {
@ -491,10 +470,37 @@ export class AIToolService {
return new AIToolResponsePayload({ result: await this.memoriesService.updateMemories(content) });
}
/** Helpers **/
private async collectDeletionCandidatesArtifacts(filePaths: string[]): Promise<Artifact[]> {
const artifacts: Artifact[] = [];
for (const filePath of filePaths) {
const fileType = toFileType(extname(filePath));
// Anything that isn't a note we will just store as a binary artifact
if (isBinaryFile(fileType) || isDocumentMimeType(FileTypeToMimeType[fileType])) {
const result = await this.fileSystemService.readBinaryFile(filePath);
if (result instanceof ArrayBuffer) {
artifacts.push(new Artifact(filePath, FileTypeToMimeType[fileType], ArtifactAction.Delete, "", "", arrayBufferToBase64(result)));
}
} else {
const result = await this.fileSystemService.readFilePath(filePath);
if (typeof result === "string") {
artifacts.push(new Artifact(filePath, FileTypeToMimeType[fileType], ArtifactAction.Delete, result, ""));
}
}
}
return artifacts;
}
private async asTrackedAction(filePath: string, action: () => Promise<TFile|Error|void>): Promise<AIToolResponsePayload> {
let artifactAction: ArtifactAction | undefined;
let preActionResult = await this.fileSystemService.readFilePath(filePath);
if (preActionResult instanceof Error) {
preActionResult = ""; // The file does not exist yet
artifactAction = ArtifactAction.Create;
}
const actionResult = await action();
if (actionResult instanceof Error) {
@ -503,11 +509,16 @@ export class AIToolService {
let postActionResult = actionResult ? await this.fileSystemService.readFile(actionResult) : "";
if (postActionResult instanceof Error) {
postActionResult = ""; // The file has been deleted
artifactAction = ArtifactAction.Delete;
}
if (artifactAction === undefined) {
artifactAction = ArtifactAction.Modify;
}
const fileType = toFileType(extname(filePath));
return new AIToolResponsePayload({ success: true },
[new Artifact(filePath, FileTypeToMimeType[fileType], preActionResult, postActionResult)]);
[new Artifact(filePath, FileTypeToMimeType[fileType], artifactAction, preActionResult, postActionResult)]);
}
}

View file

@ -76,6 +76,7 @@ export class ConversationFileSystemService {
artifacts: content.artifacts.map(artifact => ({
filePath: artifact.filePath,
mimeType: artifact.mimeType,
action: artifact.action,
originalContent: artifact.originalContent,
updatedContent: artifact.updatedContent,
artifactPath: artifact.artifactPath
@ -406,6 +407,7 @@ export class ConversationFileSystemService {
artifacts.push(new Artifact(
artifactData.filePath,
artifactData.mimeType,
artifactData.action,
artifactData.originalContent,
artifactData.updatedContent,
base64,

View file

@ -42,12 +42,15 @@ export class VaultCacheService {
this.metaDataCache = this.plugin.app.metadataCache;
this.registerFileEvents();
this.plugin.app.metadataCache.on("resolved", async () => {
const tryInitialise = async () => {
if (!this.initialised) {
await this.setupCaches();
this.initialised = true;
await this.setupCaches();
}
});
};
this.plugin.app.metadataCache.on("resolved", tryInitialise);
void tryInitialise();
}
public matchTag(input: string): Fuzzysort.KeyResults<{ prepared: Fuzzysort.Prepared, tag: string }> {

View file

@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import { Artifact } from '../../Conversations/Artifact';
import { ArtifactAction } from '../../Enums/ArtifactAction';
/**
* UNIT TESTS - Artifact
@ -11,10 +12,11 @@ import { Artifact } from '../../Conversations/Artifact';
describe('Artifact', () => {
describe('constructor', () => {
it('should create an artifact with required fields', () => {
const artifact = new Artifact('notes/test.md', 'text/markdown', 'old content', 'new content');
const artifact = new Artifact('notes/test.md', 'text/markdown', ArtifactAction.Modify, 'old content', 'new content');
expect(artifact.filePath).toBe('notes/test.md');
expect(artifact.mimeType).toBe('text/markdown');
expect(artifact.action).toBe(ArtifactAction.Modify);
expect(artifact.originalContent).toBe('old content');
expect(artifact.updatedContent).toBe('new content');
expect(artifact.base64).toBeUndefined();
@ -25,6 +27,7 @@ describe('Artifact', () => {
const artifact = new Artifact(
'image.png',
'image/png',
ArtifactAction.Modify,
'',
'',
'base64data==',
@ -36,14 +39,14 @@ describe('Artifact', () => {
});
it('should allow empty originalContent for newly created files', () => {
const artifact = new Artifact('new-file.md', 'text/markdown', '', 'Some new content');
const artifact = new Artifact('new-file.md', 'text/markdown', ArtifactAction.Create, '', 'Some new content');
expect(artifact.originalContent).toBe('');
expect(artifact.updatedContent).toBe('Some new content');
});
it('should allow empty updatedContent for deleted files', () => {
const artifact = new Artifact('deleted-file.md', 'text/markdown', 'Content before deletion', '');
const artifact = new Artifact('deleted-file.md', 'text/markdown', ArtifactAction.Delete, 'Content before deletion', '');
expect(artifact.originalContent).toBe('Content before deletion');
expect(artifact.updatedContent).toBe('');
@ -52,19 +55,19 @@ describe('Artifact', () => {
describe('getStoragePath / setStoragePath', () => {
it('should return undefined when no storage path has been set', () => {
const artifact = new Artifact('test.md', 'text/markdown', 'a', 'b');
const artifact = new Artifact('test.md', 'text/markdown', ArtifactAction.Modify, 'a', 'b');
expect(artifact.getStoragePath()).toBeUndefined();
});
it('should return the artifactPath passed to the constructor', () => {
const artifact = new Artifact('test.md', 'text/markdown', 'a', 'b', 'base64', 'Artifacts/existing.bin');
const artifact = new Artifact('test.md', 'text/markdown', ArtifactAction.Modify, 'a', 'b', 'base64', 'Artifacts/existing.bin');
expect(artifact.getStoragePath()).toBe('Artifacts/existing.bin');
});
it('should update the storage path when set', () => {
const artifact = new Artifact('test.md', 'text/markdown', 'a', 'b');
const artifact = new Artifact('test.md', 'text/markdown', ArtifactAction.Modify, 'a', 'b');
artifact.setStoragePath('Artifacts/newly-saved.bin');
@ -73,7 +76,7 @@ describe('Artifact', () => {
});
it('should overwrite an existing storage path', () => {
const artifact = new Artifact('test.md', 'text/markdown', 'a', 'b', undefined, 'Artifacts/old.bin');
const artifact = new Artifact('test.md', 'text/markdown', ArtifactAction.Modify, 'a', 'b', undefined, 'Artifacts/old.bin');
artifact.setStoragePath('Artifacts/new.bin');
@ -86,6 +89,7 @@ describe('Artifact', () => {
const data = {
filePath: 'test.md',
mimeType: 'text/markdown',
action: ArtifactAction.Modify,
originalContent: 'old',
updatedContent: 'new'
};
@ -97,6 +101,7 @@ describe('Artifact', () => {
const data = {
filePath: 'test.md',
mimeType: 'text/markdown',
action: ArtifactAction.Modify,
originalContent: 'old',
updatedContent: 'new',
base64: 'YWJj',
@ -129,6 +134,30 @@ describe('Artifact', () => {
it('should return false when mimeType is missing', () => {
const data = {
filePath: 'test.md',
action: ArtifactAction.Modify,
originalContent: 'old',
updatedContent: 'new'
};
expect(Artifact.isArtifactData(data)).toBe(false);
});
it('should return false when action is missing', () => {
const data = {
filePath: 'test.md',
mimeType: 'text/markdown',
originalContent: 'old',
updatedContent: 'new'
};
expect(Artifact.isArtifactData(data)).toBe(false);
});
it('should return false when action is not a valid ArtifactAction', () => {
const data = {
filePath: 'test.md',
mimeType: 'text/markdown',
action: 'not-a-valid-action',
originalContent: 'old',
updatedContent: 'new'
};
@ -140,6 +169,7 @@ describe('Artifact', () => {
const data = {
filePath: 'test.md',
mimeType: 'text/markdown',
action: ArtifactAction.Modify,
updatedContent: 'new'
};
@ -150,6 +180,7 @@ describe('Artifact', () => {
const data = {
filePath: 'test.md',
mimeType: 'text/markdown',
action: ArtifactAction.Modify,
originalContent: 'old'
};
@ -160,6 +191,7 @@ describe('Artifact', () => {
const data = {
filePath: 123,
mimeType: 'text/markdown',
action: ArtifactAction.Modify,
originalContent: 'old',
updatedContent: 'new'
};
@ -171,6 +203,7 @@ describe('Artifact', () => {
const data = {
filePath: 'test.md',
mimeType: 'text/markdown',
action: ArtifactAction.Modify,
originalContent: 'old',
updatedContent: 'new',
base64: 12345
@ -183,6 +216,7 @@ describe('Artifact', () => {
const data = {
filePath: 'test.md',
mimeType: 'text/markdown',
action: ArtifactAction.Modify,
originalContent: 'old',
updatedContent: 'new',
artifactPath: true
@ -195,6 +229,7 @@ describe('Artifact', () => {
const data = {
filePath: 'new-file.md',
mimeType: 'text/markdown',
action: ArtifactAction.Create,
originalContent: '',
updatedContent: ''
};

View file

@ -8,6 +8,7 @@ import { Role } from '../../Enums/Role';
import { TFile } from 'obsidian';
import { Exception } from '../../Helpers/Exception';
import { Artifact } from '../../Conversations/Artifact';
import { ArtifactAction } from '../../Enums/ArtifactAction';
/**
* INTEGRATION TESTS - ConversationFileSystemService
@ -533,7 +534,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
describe('Artifacts', () => {
it('should save an artifact with base64 content as a binary file and store its path', async () => {
const conversation = createTestConversation('With Artifact');
const artifact = new Artifact('notes/test.md', 'text/markdown', 'old', 'new', 'YWJjZGVm');
const artifact = new Artifact('notes/test.md', 'text/markdown', ArtifactAction.Modify, 'old', 'new', 'YWJjZGVm');
conversation.contents.push(
new ConversationContent({ role: Role.Assistant, content: 'Edited', artifacts: [artifact] })
@ -554,6 +555,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
expect(savedArtifact).toMatchObject({
filePath: 'notes/test.md',
mimeType: 'text/markdown',
action: ArtifactAction.Modify,
originalContent: 'old',
updatedContent: 'new',
artifactPath: artifact.artifactPath
@ -562,7 +564,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
it('should not write a binary file for artifacts without base64 content (text-only edits)', async () => {
const conversation = createTestConversation('Text Only Artifact');
const artifact = new Artifact('notes/test.md', 'text/markdown', 'old', 'new');
const artifact = new Artifact('notes/test.md', 'text/markdown', ArtifactAction.Modify, 'old', 'new');
conversation.contents.push(
new ConversationContent({ role: Role.Assistant, content: 'Edited', artifacts: [artifact] })
@ -576,7 +578,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
it('should not re-write a binary file for an artifact that already has a storage path', async () => {
const conversation = createTestConversation('Existing Artifact');
const artifact = new Artifact('notes/test.md', 'text/markdown', 'old', 'new', 'YWJjZGVm', 'Artifacts/existing-hash.bin');
const artifact = new Artifact('notes/test.md', 'text/markdown', ArtifactAction.Modify, 'old', 'new', 'YWJjZGVm', 'Artifacts/existing-hash.bin');
conversation.contents.push(
new ConversationContent({ role: Role.Assistant, content: 'Edited', artifacts: [artifact] })
@ -604,6 +606,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
{
filePath: 'notes/test.md',
mimeType: 'text/markdown',
action: ArtifactAction.Modify,
originalContent: 'old',
updatedContent: 'new',
artifactPath: 'Artifacts/hash123.bin'
@ -644,6 +647,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
{
filePath: 'notes/test.md',
mimeType: 'text/markdown',
action: ArtifactAction.Modify,
originalContent: 'old',
updatedContent: 'new'
}
@ -707,6 +711,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
{
filePath: 'notes/test.md',
mimeType: 'text/markdown',
action: ArtifactAction.Modify,
originalContent: 'old',
updatedContent: 'new',
artifactPath: 'Artifacts/referenced.bin'
@ -758,6 +763,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
{
filePath: 'notes/test.md',
mimeType: 'text/markdown',
action: ArtifactAction.Modify,
originalContent: 'old',
updatedContent: 'new',
artifactPath: 'Artifacts/kept.bin'