FN-8755: add undo and redo controls to file editor

Add localized CodeMirror history controls while preserving current editor mode during external content resets.

- Add native undo/redo commands, toolbar controls, keyboard shortcuts, and translations
- Reset history for accepted external replacements while retaining stale self-echo history
- Cover simultaneous content and read-only transitions and document editor shortcuts

Files changed:
 .changeset/fn-8755-file-editor-history.md          |   7 ++
 docs/dashboard-guide.md                            |   2 +-
 packages/dashboard/app/components/FileEditor.tsx   |  96 ++++++++++++------
 .../app/components/__tests__/FileEditor.test.tsx   | 108 +++++++++++++++++++++
 packages/dashboard/package.json                    |   3 +-
 packages/i18n/locales/en/app.json                  |   2 +
 packages/i18n/locales/es/app.json                  |  20 +++-
 packages/i18n/locales/fr/app.json                  |  20 +++-
 packages/i18n/locales/ko/app.json                  |  20 +++-
 packages/i18n/locales/zh-CN/app.json               |  20 +++-
 packages/i18n/locales/zh-TW/app.json               |  20 +++-
 packages/i18n/src/resources.d.ts                   |   2 +
 pnpm-lock.yaml                                     |  45 ++++++---
 13 files changed, 294 insertions(+), 71 deletions(-)

Fusion-Task-Id: FN-8755

Fusion-Task-Lineage: 1c0bdb9b-f866-4e01-8002-afe27f34d670

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-08-02 23:44:22 -07:00
parent a6d64eba8c
commit af66b382e0
13 changed files with 294 additions and 71 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Add undo and redo controls to editable file editors.
category: feature
dev: Uses CodeMirror native history with controlled external-content baselines.

View File

@@ -1155,7 +1155,7 @@ The Files modal provides a workspace-aware file browser and editor.
## Memory View
Memory view provides a multi-file editor for project and daily memory files. Its file editors share the same highlighted-text **Add comment** affordance as the Files modal, so memory snippets can seed a New Task with file path, snippet, and comment context.
Memory view provides a multi-file editor for project and daily memory files. Its file editors share the same highlighted-text **Add comment** affordance as the Files modal, so memory snippets can seed a New Task with file path, snippet, and comment context. Editable file editors also provide localized **Undo** and **Redo** toolbar controls and the standard platform shortcuts: <kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Z</kbd> to undo, plus <kbd>Ctrl</kbd>+<kbd>Y</kbd> or <kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> to redo.
> Available when the `experimentalFeatures.memoryView` toggle is enabled.

View File

@@ -2,9 +2,10 @@ import { useState, useCallback, useMemo, useRef, useId, useEffect } from "react"
import { useTranslation } from "react-i18next";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { FileEdit, Eye, ListOrdered, WrapText, ChevronDown, ChevronUp, Save } from "lucide-react";
import { EditorView, lineNumbers } from "@codemirror/view";
import { FileEdit, Eye, ListOrdered, WrapText, ChevronDown, ChevronUp, Save, Undo2, Redo2 } from "lucide-react";
import { EditorView, lineNumbers, keymap } from "@codemirror/view";
import { EditorState, Compartment, type Extension } from "@codemirror/state";
import { history, historyKeymap, redo, redoDepth, undo, undoDepth } from "@codemirror/commands";
import { syntaxHighlighting, defaultHighlightStyle } from "@codemirror/language";
import { oneDark } from "@codemirror/theme-one-dark";
import { useSelectionComment } from "../hooks/useSelectionComment";
@@ -99,6 +100,7 @@ export function FileEditor({
const editorHostRef = useRef<HTMLDivElement>(null);
const previewRef = useRef<HTMLDivElement>(null);
const editorViewRef = useRef<EditorView | null>(null);
const createEditorStateRef = useRef<((doc: string, selection?: { anchor: number; head: number }) => EditorState) | null>(null);
const syncingFromPropsRef = useRef(false);
const localEditVersionRef = useRef(0);
const contentEditVersionsRef = useRef<Map<string, number>>(new Map([[content, 0]]));
@@ -111,6 +113,7 @@ export function FileEditor({
const readOnlyCompartmentRef = useRef(new Compartment());
const languageCompartmentRef = useRef(new Compartment());
const themeCompartmentRef = useRef(new Compartment());
const [historyAvailability, setHistoryAvailability] = useState({ undo: false, redo: false });
const isMarkdown = isMarkdownFile(filePath);
const generatedToolbarActionsId = useId();
@@ -121,8 +124,11 @@ export function FileEditor({
const shouldRenderLineNumbers = showLineNumbers && !readOnly && !effectiveShowPreview;
const shouldShowLineNumbersToggle = Boolean(onToggleLineNumbers) && canToggleLineNumbers && !readOnly && !effectiveShowPreview;
const shouldShowAutoSaveToggle = Boolean(onToggleAutoSave) && canToggleAutoSave && !readOnly && !effectiveShowPreview;
const shouldShowHistoryControls = !readOnly && !effectiveShowPreview;
const hasToolbarActions = isMarkdown || !readOnly || shouldShowLineNumbersToggle || shouldShowAutoSaveToggle;
const languageExtension = useMemo(() => resolveCodeMirrorLanguage(filePath), [filePath]);
const editorStateConfigRef = useRef({ shouldRenderLineNumbers, wordWrap, readOnly, languageExtension, darkThemeActive });
editorStateConfigRef.current = { shouldRenderLineNumbers, wordWrap, readOnly, languageExtension, darkThemeActive };
const handleEditClick = useCallback(() => setShowPreview(false), []);
const handlePreviewClick = useCallback(() => setShowPreview(true), []);
@@ -132,6 +138,11 @@ export function FileEditor({
setInternalExpanded((prev) => !prev);
}
}, [isControlled]);
const handleHistoryCommand = useCallback((command: typeof undo | typeof redo) => {
const view = editorViewRef.current;
if (!view || !command(view)) return;
view.focus();
}, []);
const [selectionCommentOpen, setSelectionCommentOpen] = useState(false);
const getCodeMirrorLineRange = useCallback(() => {
@@ -175,28 +186,39 @@ export function FileEditor({
"&.cm-focused": { outline: "none" },
});
const state = EditorState.create({
doc: content,
extensions: [
lineNumbersCompartmentRef.current.of(shouldRenderLineNumbers ? lineNumbers() : []),
wordWrapCompartmentRef.current.of(wordWrap ? EditorView.lineWrapping : []),
readOnlyCompartmentRef.current.of(readOnly ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : []),
languageCompartmentRef.current.of(languageExtension ?? []),
themeCompartmentRef.current.of(buildThemeExtension(darkThemeActive)),
themeOverlay,
EditorView.updateListener.of((update) => {
if (!update.docChanged || syncingFromPropsRef.current) return;
const nextContent = update.state.doc.toString();
localEditVersionRef.current += 1;
contentEditVersionsRef.current.set(nextContent, localEditVersionRef.current);
onChangeRef.current(nextContent);
}),
],
});
const createEditorState = (doc: string, selection?: { anchor: number; head: number }) => {
const { shouldRenderLineNumbers, wordWrap, readOnly, languageExtension, darkThemeActive } = editorStateConfigRef.current;
return EditorState.create({
doc,
selection,
extensions: [
history(),
keymap.of(historyKeymap),
lineNumbersCompartmentRef.current.of(shouldRenderLineNumbers ? lineNumbers() : []),
wordWrapCompartmentRef.current.of(wordWrap ? EditorView.lineWrapping : []),
readOnlyCompartmentRef.current.of(readOnly ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : []),
languageCompartmentRef.current.of(languageExtension ?? []),
themeCompartmentRef.current.of(buildThemeExtension(darkThemeActive)),
themeOverlay,
EditorView.updateListener.of((update) => {
if (update.docChanged || update.transactions.some((transaction) => transaction.effects.length > 0)) {
setHistoryAvailability({ undo: undoDepth(update.state) > 0, redo: redoDepth(update.state) > 0 });
}
if (!update.docChanged || syncingFromPropsRef.current) return;
const nextContent = update.state.doc.toString();
localEditVersionRef.current += 1;
contentEditVersionsRef.current.set(nextContent, localEditVersionRef.current);
onChangeRef.current(nextContent);
}),
],
});
};
const view = new EditorView({ state, parent: editorHostRef.current });
createEditorStateRef.current = createEditorState;
const view = new EditorView({ state: createEditorState(content), parent: editorHostRef.current });
editorViewRef.current = view;
return () => {
createEditorStateRef.current = null;
editorViewRef.current = null;
view.destroy();
};
@@ -255,6 +277,14 @@ export function FileEditor({
});
}, [darkThemeActive]);
/*
* FNXC:FileEditor 2026-08-03-06:15:
* Editable editors expose native CodeMirror undo/redo through localized controls and platform shortcuts, while read-only and markdown preview presentations expose no editing affordances. A stale controlled self-echo preserves its local history, but an accepted external replacement resets the native editor state so it becomes a non-undoable baseline and cannot restore stale content.
*
* FNXC:FileEditor 2026-08-03-06:41:
* External content and read-only mode may change in the same render. Build replacement states from the current render configuration rather than the initial editor mount so the reset never reverses the caller's editability choice.
*/
/*
* FNXC:FileViewer 2026-07-10-22:52:
* FN-7810 found that the bounded self-echo Set could evict a stale value during long sessions, and it could miss end-of-file Enter flows where a trailing header newline raced an older prop. Use monotonic edit versions for every local CodeMirror emission instead: any prop whose known version is older than the live editor or last accepted prop is a stale self-echo at any session length, while never-emitted external reload/save-normalization content still replaces the document with the caret clamped into range.
@@ -280,13 +310,13 @@ export function FileEditor({
const clampPosition = (position: number) => Math.max(0, Math.min(position, nextLength));
syncingFromPropsRef.current = true;
try {
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: content },
selection: {
anchor: clampPosition(previousSelection.anchor),
head: clampPosition(previousSelection.head),
},
});
const createEditorState = createEditorStateRef.current;
if (!createEditorState) return;
view.setState(createEditorState(content, {
anchor: clampPosition(previousSelection.anchor),
head: clampPosition(previousSelection.head),
}));
setHistoryAvailability({ undo: false, redo: false });
const externalContentVersion = localEditVersionRef.current;
contentEditVersionsRef.current.set(content, externalContentVersion);
lastPropEditVersionRef.current = Math.max(lastPropEditVersionRef.current, externalContentVersion);
@@ -319,6 +349,16 @@ export function FileEditor({
</button>
</>
) : null}
{shouldShowHistoryControls && (
<>
<button className="btn btn-sm btn-icon file-editor-toolbar-button" onClick={() => handleHistoryCommand(undo)} disabled={!historyAvailability.undo} aria-label={t("fileEditor.undo", "Undo")} title={t("fileEditor.undo", "Undo")}>
<Undo2 />
</button>
<button className="btn btn-sm btn-icon file-editor-toolbar-button" onClick={() => handleHistoryCommand(redo)} disabled={!historyAvailability.redo} aria-label={t("fileEditor.redo", "Redo")} title={t("fileEditor.redo", "Redo")}>
<Redo2 />
</button>
</>
)}
{shouldShowAutoSaveToggle && (
<button className={`btn btn-sm file-editor-toolbar-button ${autoSaveEnabled ? "btn-primary" : ""}`} onClick={onToggleAutoSave} aria-label={t("fileEditor.toggleAutoSave", "Toggle auto-save")} aria-pressed={autoSaveEnabled} title={t("fileEditor.toggleAutoSave", "Toggle auto-save")} data-testid="file-editor-auto-save-toggle">
<Save size={14} />

View File

@@ -2,6 +2,8 @@ import { useState } from "react";
import { afterEach, beforeEach, describe, it, expect, vi } from "vitest";
import { act, render, screen, fireEvent, waitFor } from "@testing-library/react";
import { EditorView } from "@codemirror/view";
import i18next from "i18next";
import esApp from "../../../../i18n/locales/es/app.json";
import { loadAllAppCss } from "../../test/cssFixture";
import { FileEditor } from "../FileEditor";
@@ -92,6 +94,94 @@ describe("FileEditor", () => {
expect(onChange).toHaveBeenCalledWith("new content");
});
it("undoes and redoes local edits through toolbar and standard keyboard commands", async () => {
document.documentElement.dataset.theme = "dark";
const onChange = vi.fn();
render(<FileEditor content="start" onChange={onChange} filePath="a.ts" />);
const view = getEditorView();
act(() => {
view.dispatch({ changes: { from: 5, insert: " one" } });
view.dispatch({ changes: { from: 0, insert: "two " } });
});
expect(view.state.doc.toString()).toBe("two start one");
expandEditorOptions();
const undoButton = screen.getByRole("button", { name: "Undo" });
const redoButton = screen.getByRole("button", { name: "Redo" });
await waitFor(() => expect(undoButton).toBeEnabled());
expect(redoButton).toBeDisabled();
fireEvent.click(undoButton);
expect(view.state.doc.toString()).toBe("start one");
expect(onChange).toHaveBeenLastCalledWith("start one");
expect(redoButton).toBeEnabled();
fireEvent.keyDown(view.contentDOM, { key: "y", ctrlKey: true });
expect(view.state.doc.toString()).toBe("two start one");
expect(onChange).toHaveBeenLastCalledWith("two start one");
fireEvent.click(undoButton);
act(() => {
view.dispatch({ changes: { from: 0, insert: "branch " } });
});
expect(view.state.doc.toString()).toBe("branch start one");
expect(redoButton).toBeDisabled();
});
it("clears history for accepted external replacements but preserves it for stale self-echoes", async () => {
document.documentElement.dataset.theme = "dark";
const onChange = vi.fn();
const { rerender } = render(<FileEditor content="start" onChange={onChange} filePath="a.ts" />);
const view = getEditorView();
act(() => {
view.dispatch({ changes: { from: 5, insert: " local" } });
});
rerender(<FileEditor content="start local" onChange={onChange} filePath="a.ts" />);
act(() => {
view.dispatch({ changes: { from: 11, insert: " latest" } });
});
rerender(<FileEditor content="start local" onChange={onChange} filePath="a.ts" />);
await waitFor(() => expect(getEditorView().state.doc.toString()).toBe("start local latest"));
expandEditorOptions();
await waitFor(() => expect(screen.getByRole("button", { name: "Undo" })).toBeEnabled());
rerender(<FileEditor content="replacement" onChange={onChange} filePath="a.ts" />);
await waitFor(() => expect(getEditorView().state.doc.toString()).toBe("replacement"));
const undoButton = screen.getByRole("button", { name: "Undo" });
const redoButton = screen.getByRole("button", { name: "Redo" });
expect(undoButton).toBeDisabled();
expect(redoButton).toBeDisabled();
fireEvent.click(undoButton);
fireEvent.click(redoButton);
expect(getEditorView().state.doc.toString()).toBe("replacement");
expect(onChange).not.toHaveBeenLastCalledWith("replacement");
act(() => {
getEditorView().dispatch({ changes: { from: 11, insert: " edited" } });
});
fireEvent.click(undoButton);
expect(getEditorView().state.doc.toString()).toBe("replacement");
});
it("uses translated undo and redo accessible names without falling back to English", async () => {
await i18next.addResourceBundle("es", "app", esApp, true, true);
await i18next.changeLanguage("es");
try {
render(<FileEditor content="start" onChange={vi.fn()} filePath="a.ts" />);
act(() => {
getEditorView().dispatch({ changes: { from: 5, insert: " edit" } });
});
fireEvent.click(document.querySelector(".file-editor-toolbar-button")!);
expect(screen.getByRole("button", { name: "Deshacer" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Rehacer" })).toBeDisabled();
} finally {
await i18next.changeLanguage("en");
}
});
it("preserves the editor instance across content prop updates", async () => {
document.documentElement.dataset.theme = "dark";
const { rerender } = render(<FileEditor content="alpha" onChange={vi.fn()} filePath="a.ts" />);
@@ -253,6 +343,24 @@ describe("FileEditor", () => {
expect(document.querySelector(".cm-content")?.getAttribute("contenteditable")).toBe("false");
});
it("keeps current read-only mode when external content resets editor history", async () => {
document.documentElement.dataset.theme = "dark";
const onChange = vi.fn();
const { rerender } = render(<FileEditor content="editable" onChange={onChange} filePath="a.ts" />);
rerender(<FileEditor content="locked replacement" onChange={onChange} readOnly filePath="a.ts" />);
await waitFor(() => {
expect(getEditorView().state.doc.toString()).toBe("locked replacement");
expect(document.querySelector(".cm-content")?.getAttribute("contenteditable")).toBe("false");
});
rerender(<FileEditor content="editable replacement" onChange={onChange} filePath="a.ts" />);
await waitFor(() => {
expect(getEditorView().state.doc.toString()).toBe("editable replacement");
expect(document.querySelector(".cm-content")?.getAttribute("contenteditable")).toBe("true");
});
});
it("uses fallback aria-label when filePath missing", () => {
document.documentElement.dataset.theme = "dark";
render(<FileEditor content="x" onChange={vi.fn()} />);

View File

@@ -99,12 +99,13 @@
"test:quality:api:backfill-2": "node scripts/run-vitest-with-heap.mjs --heap=6144 run --project dashboard-api-quality-backfill --silent=passed-only --reporter=dot --shard=2/2"
},
"dependencies": {
"@codemirror/commands": "^6.10.4",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.2",
"@codemirror/language": "^6.12.3",
"@codemirror/state": "^6.5.2",
"@codemirror/state": "^6.7.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.36.4",
"@earendil-works/pi-coding-agent": "0.82.1",

View File

@@ -2494,6 +2494,8 @@
"lineNumber": "Line #",
"preview": "Preview",
"previewMode": "Preview mode",
"redo": "Redo",
"undo": "Undo",
"toggleAutoSave": "Toggle auto-save",
"toggleLineNumbers": "Toggle line numbers",
"toggleOptions": "Toggle editor options",

View File

@@ -2484,6 +2484,8 @@
"lineNumber": "Línea #",
"preview": "Vista previa",
"previewMode": "Modo vista previa",
"redo": "Rehacer",
"undo": "Deshacer",
"toggleAutoSave": "",
"toggleLineNumbers": "Alternar números de línea",
"toggleOptions": "Alternar opciones del editor",
@@ -6054,9 +6056,7 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": "",
"autoUpdateAndRestart": "",
"autoUpdateAndRestartHelp": ""
"noLimitOnAgentToolOutputHint": ""
},
"globalModels": {
"allow": "",
@@ -6539,7 +6539,11 @@
"selectChatDefaultAgent": "Select a chat default agent",
"chatDefaultAgentEmpty": "No agents are available for this project yet.",
"chatDefaultAgentHelp": "Agent-mode New Chat starts a Direct chat with the selected durable agent.",
"chatDefaultReset": "Reset Chat default"
"chatDefaultReset": "Reset Chat default",
"executorEscalationModel": "",
"executorEscalationModelHelp": "",
"selectExecutorEscalationModel": "",
"noExecutorEscalationModel": ""
},
"remote": {
"acceptRoutes": "",
@@ -6923,7 +6927,13 @@
"notInstalled": "Not installed",
"download": "Download",
"remove": "Remove",
"error": "Model error: {{message}}"
"error": "Model error: {{message}}",
"modelRequired": "",
"modelPreparing": "",
"modelFailed": "",
"runtimeModuleMissing": "",
"runtimePlatformLoadFailed": "",
"runtimeIncompatible": ""
}
},
"setup": {

View File

@@ -2484,6 +2484,8 @@
"lineNumber": "Ligne #",
"preview": "Aperçu",
"previewMode": "Mode aperçu",
"redo": "Rétablir",
"undo": "Annuler",
"toggleAutoSave": "",
"toggleLineNumbers": "Basculer les numéros de ligne",
"toggleOptions": "Basculer les options de l'éditeur",
@@ -6054,9 +6056,7 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": "",
"autoUpdateAndRestart": "",
"autoUpdateAndRestartHelp": ""
"noLimitOnAgentToolOutputHint": ""
},
"globalModels": {
"allow": "",
@@ -6539,7 +6539,11 @@
"selectChatDefaultAgent": "Select a chat default agent",
"chatDefaultAgentEmpty": "No agents are available for this project yet.",
"chatDefaultAgentHelp": "Agent-mode New Chat starts a Direct chat with the selected durable agent.",
"chatDefaultReset": "Reset Chat default"
"chatDefaultReset": "Reset Chat default",
"executorEscalationModel": "",
"executorEscalationModelHelp": "",
"selectExecutorEscalationModel": "",
"noExecutorEscalationModel": ""
},
"remote": {
"acceptRoutes": "",
@@ -6923,7 +6927,13 @@
"notInstalled": "Not installed",
"download": "Download",
"remove": "Remove",
"error": "Model error: {{message}}"
"error": "Model error: {{message}}",
"modelRequired": "",
"modelPreparing": "",
"modelFailed": "",
"runtimeModuleMissing": "",
"runtimePlatformLoadFailed": "",
"runtimeIncompatible": ""
}
},
"setup": {

View File

@@ -2484,6 +2484,8 @@
"lineNumber": "줄 번호",
"preview": "미리보기",
"previewMode": "미리보기 모드",
"redo": "다시 실행",
"undo": "실행 취소",
"toggleAutoSave": "",
"toggleLineNumbers": "줄 번호 표시 토글",
"toggleOptions": "편집기 옵션 토글",
@@ -6054,9 +6056,7 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": "",
"autoUpdateAndRestart": "",
"autoUpdateAndRestartHelp": ""
"noLimitOnAgentToolOutputHint": ""
},
"globalModels": {
"allow": "",
@@ -6539,7 +6539,11 @@
"selectChatDefaultAgent": "Select a chat default agent",
"chatDefaultAgentEmpty": "No agents are available for this project yet.",
"chatDefaultAgentHelp": "Agent-mode New Chat starts a Direct chat with the selected durable agent.",
"chatDefaultReset": "Reset Chat default"
"chatDefaultReset": "Reset Chat default",
"executorEscalationModel": "",
"executorEscalationModelHelp": "",
"selectExecutorEscalationModel": "",
"noExecutorEscalationModel": ""
},
"remote": {
"acceptRoutes": "",
@@ -6923,7 +6927,13 @@
"notInstalled": "Not installed",
"download": "Download",
"remove": "Remove",
"error": "Model error: {{message}}"
"error": "Model error: {{message}}",
"modelRequired": "",
"modelPreparing": "",
"modelFailed": "",
"runtimeModuleMissing": "",
"runtimePlatformLoadFailed": "",
"runtimeIncompatible": ""
}
},
"setup": {

View File

@@ -2484,6 +2484,8 @@
"lineNumber": "行号",
"preview": "预览",
"previewMode": "预览模式",
"redo": "重做",
"undo": "撤销",
"toggleAutoSave": "",
"toggleLineNumbers": "切换行号",
"toggleOptions": "切换编辑器选项",
@@ -6054,9 +6056,7 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": "",
"autoUpdateAndRestart": "",
"autoUpdateAndRestartHelp": ""
"noLimitOnAgentToolOutputHint": ""
},
"globalModels": {
"allow": "",
@@ -6539,7 +6539,11 @@
"selectChatDefaultAgent": "Select a chat default agent",
"chatDefaultAgentEmpty": "No agents are available for this project yet.",
"chatDefaultAgentHelp": "Agent-mode New Chat starts a Direct chat with the selected durable agent.",
"chatDefaultReset": "Reset Chat default"
"chatDefaultReset": "Reset Chat default",
"executorEscalationModel": "",
"executorEscalationModelHelp": "",
"selectExecutorEscalationModel": "",
"noExecutorEscalationModel": ""
},
"remote": {
"acceptRoutes": "",
@@ -6923,7 +6927,13 @@
"notInstalled": "Not installed",
"download": "Download",
"remove": "Remove",
"error": "Model error: {{message}}"
"error": "Model error: {{message}}",
"modelRequired": "",
"modelPreparing": "",
"modelFailed": "",
"runtimeModuleMissing": "",
"runtimePlatformLoadFailed": "",
"runtimeIncompatible": ""
}
},
"setup": {

View File

@@ -2484,6 +2484,8 @@
"lineNumber": "行號",
"preview": "預覽",
"previewMode": "預覽模式",
"redo": "重做",
"undo": "復原",
"toggleAutoSave": "",
"toggleLineNumbers": "切換行號",
"toggleOptions": "切換編輯器選項",
@@ -6054,9 +6056,7 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": "",
"autoUpdateAndRestart": "",
"autoUpdateAndRestartHelp": ""
"noLimitOnAgentToolOutputHint": ""
},
"globalModels": {
"allow": "",
@@ -6539,7 +6539,11 @@
"selectChatDefaultAgent": "Select a chat default agent",
"chatDefaultAgentEmpty": "No agents are available for this project yet.",
"chatDefaultAgentHelp": "Agent-mode New Chat starts a Direct chat with the selected durable agent.",
"chatDefaultReset": "Reset Chat default"
"chatDefaultReset": "Reset Chat default",
"executorEscalationModel": "",
"executorEscalationModelHelp": "",
"selectExecutorEscalationModel": "",
"noExecutorEscalationModel": ""
},
"remote": {
"acceptRoutes": "",
@@ -6923,7 +6927,13 @@
"notInstalled": "Not installed",
"download": "Download",
"remove": "Remove",
"error": "Model error: {{message}}"
"error": "Model error: {{message}}",
"modelRequired": "",
"modelPreparing": "",
"modelFailed": "",
"runtimeModuleMissing": "",
"runtimePlatformLoadFailed": "",
"runtimeIncompatible": ""
}
},
"setup": {

View File

@@ -2496,10 +2496,12 @@ export default interface Resources {
"lineNumber": "Line #",
"preview": "Preview",
"previewMode": "Preview mode",
"redo": "Redo",
"toggleAutoSave": "Toggle auto-save",
"toggleLineNumbers": "Toggle line numbers",
"toggleOptions": "Toggle editor options",
"toggleWordWrap": "Toggle word wrap",
"undo": "Undo",
"wrap": "Wrap"
},
"fileMention": {

45
pnpm-lock.yaml generated
View File

@@ -239,6 +239,9 @@ importers:
packages/dashboard:
dependencies:
'@codemirror/commands':
specifier: ^6.10.4
version: 6.10.4
'@codemirror/lang-css':
specifier: ^6.3.1
version: 6.3.1
@@ -255,8 +258,8 @@ importers:
specifier: ^6.12.3
version: 6.12.3
'@codemirror/state':
specifier: ^6.5.2
version: 6.6.0
specifier: ^6.7.1
version: 6.7.1
'@codemirror/theme-one-dark':
specifier: ^6.1.2
version: 6.1.3
@@ -1730,6 +1733,9 @@ packages:
'@codemirror/autocomplete@6.20.1':
resolution: {integrity: sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==}
'@codemirror/commands@6.10.4':
resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==}
'@codemirror/lang-css@6.3.1':
resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
@@ -1751,8 +1757,8 @@ packages:
'@codemirror/lint@6.9.5':
resolution: {integrity: sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==}
'@codemirror/state@6.6.0':
resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==}
'@codemirror/state@6.7.1':
resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==}
'@codemirror/theme-one-dark@6.1.3':
resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
@@ -8758,7 +8764,14 @@ snapshots:
'@codemirror/autocomplete@6.20.1':
dependencies:
'@codemirror/language': 6.12.3
'@codemirror/state': 6.6.0
'@codemirror/state': 6.7.1
'@codemirror/view': 6.40.0
'@lezer/common': 1.5.1
'@codemirror/commands@6.10.4':
dependencies:
'@codemirror/language': 6.12.3
'@codemirror/state': 6.7.1
'@codemirror/view': 6.40.0
'@lezer/common': 1.5.1
@@ -8766,7 +8779,7 @@ snapshots:
dependencies:
'@codemirror/autocomplete': 6.20.1
'@codemirror/language': 6.12.3
'@codemirror/state': 6.6.0
'@codemirror/state': 6.7.1
'@lezer/common': 1.5.1
'@lezer/css': 1.3.3
@@ -8776,7 +8789,7 @@ snapshots:
'@codemirror/lang-css': 6.3.1
'@codemirror/lang-javascript': 6.2.5
'@codemirror/language': 6.12.3
'@codemirror/state': 6.6.0
'@codemirror/state': 6.7.1
'@codemirror/view': 6.40.0
'@lezer/common': 1.5.1
'@lezer/css': 1.3.3
@@ -8787,7 +8800,7 @@ snapshots:
'@codemirror/autocomplete': 6.20.1
'@codemirror/language': 6.12.3
'@codemirror/lint': 6.9.5
'@codemirror/state': 6.6.0
'@codemirror/state': 6.7.1
'@codemirror/view': 6.40.0
'@lezer/common': 1.5.1
'@lezer/javascript': 1.5.4
@@ -8802,14 +8815,14 @@ snapshots:
'@codemirror/autocomplete': 6.20.1
'@codemirror/lang-html': 6.4.11
'@codemirror/language': 6.12.3
'@codemirror/state': 6.6.0
'@codemirror/state': 6.7.1
'@codemirror/view': 6.40.0
'@lezer/common': 1.5.1
'@lezer/markdown': 1.6.3
'@codemirror/language@6.12.3':
dependencies:
'@codemirror/state': 6.6.0
'@codemirror/state': 6.7.1
'@codemirror/view': 6.40.0
'@lezer/common': 1.5.1
'@lezer/highlight': 1.2.3
@@ -8818,24 +8831,24 @@ snapshots:
'@codemirror/lint@6.9.5':
dependencies:
'@codemirror/state': 6.6.0
'@codemirror/state': 6.7.1
'@codemirror/view': 6.40.0
crelt: 1.0.6
'@codemirror/state@6.6.0':
'@codemirror/state@6.7.1':
dependencies:
'@marijn/find-cluster-break': 1.0.2
'@codemirror/theme-one-dark@6.1.3':
dependencies:
'@codemirror/language': 6.12.3
'@codemirror/state': 6.6.0
'@codemirror/state': 6.7.1
'@codemirror/view': 6.40.0
'@lezer/highlight': 1.2.3
'@codemirror/view@6.40.0':
dependencies:
'@codemirror/state': 6.6.0
'@codemirror/state': 6.7.1
crelt: 1.0.6
style-mod: 4.1.3
w3c-keyname: 2.2.8
@@ -8922,7 +8935,7 @@ snapshots:
'@earendil-works/pi-ai@0.82.1':
dependencies:
'@anthropic-ai/sdk': 0.91.1(zod@3.25.76)
'@anthropic-ai/sdk': 0.91.1(zod@4.4.3)
'@aws-sdk/client-bedrock-runtime': 3.1048.0
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@4.4.3))
'@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0)
@@ -8930,7 +8943,7 @@ snapshots:
'@smithy/node-http-handler': 4.7.3
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
openai: 6.26.0(ws@8.21.1)(zod@3.25.76)
openai: 6.26.0(ws@8.21.1)(zod@4.4.3)
partial-json: 0.1.7
typebox: 1.1.38
transitivePeerDependencies: