feat(FN-3528): add line-number gutter toggle to FileEditor and Files modal
This merge adds a line-number toggle to the Files modal with persisted preference, introduces an AgentAvatar component with server-side avatar storage routes, and adds documentation for both the toggle and avatar storage. It also includes a regression test for runtime plugin alias behavior and updat Fusion-Task-Id: FN-3528
This commit is contained in:
@@ -153,6 +153,13 @@ Navigation:
|
|||||||
|
|
||||||
For the full research workflow, provider setup, CLI commands, API reference, and agent integration, see the canonical [Research guide](./research.md).
|
For the full research workflow, provider setup, CLI commands, API reference, and agent integration, see the canonical [Research guide](./research.md).
|
||||||
|
|
||||||
|
## Files Modal
|
||||||
|
|
||||||
|
The Files modal provides a workspace-aware file browser and editor.
|
||||||
|
|
||||||
|
- Source/text editing supports a **Line #** header toggle to show or hide line numbers in the editor gutter
|
||||||
|
- The line-number preference is saved per project and restored automatically when you switch projects
|
||||||
|
|
||||||
## Memory View
|
## Memory View
|
||||||
|
|
||||||
Memory view provides a multi-file editor for project and daily memory files.
|
Memory view provides a multi-file editor for project and daily memory files.
|
||||||
|
|||||||
@@ -417,6 +417,35 @@
|
|||||||
padding: var(--space-lg);
|
padding: var(--space-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-editor-textarea-shell {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-editor-textarea-shell--line-numbers {
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-editor-line-numbers {
|
||||||
|
width: calc(var(--space-xl) * 2);
|
||||||
|
padding: var(--space-lg) var(--space-sm);
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
color: var(--text-dim);
|
||||||
|
background: var(--surface);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: right;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-editor-line-number {
|
||||||
|
min-height: calc(var(--space-xl) - var(--space-xs));
|
||||||
|
}
|
||||||
|
|
||||||
/* Textarea-based file editor */
|
/* Textarea-based file editor */
|
||||||
.file-editor-textarea {
|
.file-editor-textarea {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -627,6 +656,12 @@
|
|||||||
gap: var(--space-md);
|
gap: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-browser-line-numbers-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
.file-browser-modal-header .modal-close {
|
.file-browser-modal-header .modal-close {
|
||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
@@ -918,6 +953,12 @@
|
|||||||
|
|
||||||
/* Mobile responsive */
|
/* Mobile responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.file-editor-line-numbers {
|
||||||
|
width: calc(var(--space-xl) * 1.75);
|
||||||
|
padding-left: var(--space-xs);
|
||||||
|
padding-right: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
/* On mobile the file browser is presented as a full-screen sheet — drop
|
/* On mobile the file browser is presented as a full-screen sheet — drop
|
||||||
the overlay's default top padding so the modal actually fills the
|
the overlay's default top padding so the modal actually fills the
|
||||||
viewport instead of being pushed below it. */
|
viewport instead of being pushed below it. */
|
||||||
@@ -967,6 +1008,11 @@
|
|||||||
.file-browser-header-actions {
|
.file-browser-header-actions {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-line-numbers-toggle {
|
||||||
|
min-height: var(--mobile-nav-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-browser-modal-header .modal-close {
|
.file-browser-modal-header .modal-close {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import "./FileBrowser.css";
|
import "./FileBrowser.css";
|
||||||
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import { X, Save, RotateCcw, Folder, FileType, ArrowLeft } from "lucide-react";
|
import { X, Save, RotateCcw, Folder, FileType, ArrowLeft, ListOrdered } from "lucide-react";
|
||||||
import { useWorkspaceFileBrowser } from "../hooks/useWorkspaceFileBrowser";
|
import { useWorkspaceFileBrowser } from "../hooks/useWorkspaceFileBrowser";
|
||||||
import { useWorkspaceFileEditor } from "../hooks/useWorkspaceFileEditor";
|
import { useWorkspaceFileEditor } from "../hooks/useWorkspaceFileEditor";
|
||||||
import { useWorkspaces } from "../hooks/useWorkspaces";
|
import { useWorkspaces } from "../hooks/useWorkspaces";
|
||||||
@@ -10,12 +10,14 @@ import { downloadFileUrl } from "../api";
|
|||||||
import { FileBrowser } from "./FileBrowser";
|
import { FileBrowser } from "./FileBrowser";
|
||||||
import { FileEditor } from "./FileEditor";
|
import { FileEditor } from "./FileEditor";
|
||||||
import { WorkspaceSelector } from "./WorkspaceSelector";
|
import { WorkspaceSelector } from "./WorkspaceSelector";
|
||||||
|
import { getScopedItem, setScopedItem } from "../utils/projectStorage";
|
||||||
|
|
||||||
const MOBILE_BREAKPOINT = 768;
|
const MOBILE_BREAKPOINT = 768;
|
||||||
const SIDEBAR_DEFAULT_WIDTH = 280;
|
const SIDEBAR_DEFAULT_WIDTH = 280;
|
||||||
const SIDEBAR_MIN_WIDTH = 180;
|
const SIDEBAR_MIN_WIDTH = 180;
|
||||||
const SIDEBAR_MAX_WIDTH = 500;
|
const SIDEBAR_MAX_WIDTH = 500;
|
||||||
const SIDEBAR_STORAGE_KEY = "fusion:file-browser-sidebar-width";
|
const SIDEBAR_STORAGE_KEY = "fusion:file-browser-sidebar-width";
|
||||||
|
const FILES_LINE_NUMBERS_STORAGE_KEY = "kb-files-line-numbers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image file extensions that should be rendered as image previews.
|
* Image file extensions that should be rendered as image previews.
|
||||||
@@ -74,6 +76,7 @@ export function FileBrowserModal({
|
|||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
const [mobileView, setMobileView] = useState<"list" | "editor">("list");
|
const [mobileView, setMobileView] = useState<"list" | "editor">("list");
|
||||||
const [sidebarWidth, setSidebarWidth] = useState(SIDEBAR_DEFAULT_WIDTH);
|
const [sidebarWidth, setSidebarWidth] = useState(SIDEBAR_DEFAULT_WIDTH);
|
||||||
|
const [showLineNumbers, setShowLineNumbers] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
entries,
|
entries,
|
||||||
@@ -130,6 +133,11 @@ export function FileBrowserModal({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedPreference = getScopedItem(FILES_LINE_NUMBERS_STORAGE_KEY, projectId);
|
||||||
|
setShowLineNumbers(savedPreference === "true");
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
@@ -236,6 +244,14 @@ export function FileBrowserModal({
|
|||||||
persistSidebarWidth(nextWidth);
|
persistSidebarWidth(nextWidth);
|
||||||
}, [isMobile, persistSidebarWidth, sidebarWidth]);
|
}, [isMobile, persistSidebarWidth, sidebarWidth]);
|
||||||
|
|
||||||
|
const handleToggleLineNumbers = useCallback(() => {
|
||||||
|
setShowLineNumbers((previousValue) => {
|
||||||
|
const nextValue = !previousValue;
|
||||||
|
setScopedItem(FILES_LINE_NUMBERS_STORAGE_KEY, String(nextValue), projectId);
|
||||||
|
return nextValue;
|
||||||
|
});
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
const workspaceLabel = useMemo(() => {
|
const workspaceLabel = useMemo(() => {
|
||||||
if (currentWorkspace === "project") {
|
if (currentWorkspace === "project") {
|
||||||
return "Project";
|
return "Project";
|
||||||
@@ -272,6 +288,16 @@ export function FileBrowserModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="file-browser-header-actions">
|
<div className="file-browser-header-actions">
|
||||||
|
<button
|
||||||
|
className={`btn btn-sm file-browser-line-numbers-toggle ${showLineNumbers ? "btn-primary" : ""}`}
|
||||||
|
onClick={handleToggleLineNumbers}
|
||||||
|
aria-label="Toggle line numbers"
|
||||||
|
aria-pressed={showLineNumbers}
|
||||||
|
title="Toggle line numbers"
|
||||||
|
>
|
||||||
|
<ListOrdered size={14} />
|
||||||
|
<span>Line #</span>
|
||||||
|
</button>
|
||||||
<WorkspaceSelector
|
<WorkspaceSelector
|
||||||
currentWorkspace={currentWorkspace}
|
currentWorkspace={currentWorkspace}
|
||||||
projectName={projectName}
|
projectName={projectName}
|
||||||
@@ -392,6 +418,7 @@ export function FileBrowserModal({
|
|||||||
onChange={setContent}
|
onChange={setContent}
|
||||||
filePath={selectedFile}
|
filePath={selectedFile}
|
||||||
readOnly={isBinaryFile(selectedFile)}
|
readOnly={isBinaryFile(selectedFile)}
|
||||||
|
showLineNumbers={showLineNumbers && !isBinaryFile(selectedFile)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback, useMemo, useRef, type UIEvent } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { FileEdit, Eye, WrapText } from "lucide-react";
|
import { FileEdit, Eye, WrapText } from "lucide-react";
|
||||||
@@ -8,6 +8,7 @@ interface FileEditorProps {
|
|||||||
onChange: (content: string) => void;
|
onChange: (content: string) => void;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
filePath?: string;
|
filePath?: string;
|
||||||
|
showLineNumbers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMarkdownFile(filePath?: string): boolean {
|
function isMarkdownFile(filePath?: string): boolean {
|
||||||
@@ -16,13 +17,22 @@ function isMarkdownFile(filePath?: string): boolean {
|
|||||||
return lowerPath.endsWith(".md") || lowerPath.endsWith(".markdown") || lowerPath.endsWith(".mdx");
|
return lowerPath.endsWith(".md") || lowerPath.endsWith(".markdown") || lowerPath.endsWith(".mdx");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileEditor({ content, onChange, readOnly, filePath }: FileEditorProps) {
|
export function FileEditor({ content, onChange, readOnly, filePath, showLineNumbers = false }: FileEditorProps) {
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
const [wordWrap, setWordWrap] = useState(true);
|
const [wordWrap, setWordWrap] = useState(true);
|
||||||
|
const lineNumbersRef = useRef<HTMLDivElement>(null);
|
||||||
const isMarkdown = isMarkdownFile(filePath);
|
const isMarkdown = isMarkdownFile(filePath);
|
||||||
|
|
||||||
// For markdown files in readOnly mode, default to preview
|
// For markdown files in readOnly mode, default to preview
|
||||||
const effectiveShowPreview = isMarkdown && (readOnly ? true : showPreview);
|
const effectiveShowPreview = isMarkdown && (readOnly ? true : showPreview);
|
||||||
|
const shouldRenderLineNumbers = showLineNumbers && !readOnly && !effectiveShowPreview;
|
||||||
|
const lineCount = useMemo(() => {
|
||||||
|
if (!shouldRenderLineNumbers) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.split("\n").length;
|
||||||
|
}, [content, shouldRenderLineNumbers]);
|
||||||
|
|
||||||
const handleEditClick = useCallback(() => {
|
const handleEditClick = useCallback(() => {
|
||||||
setShowPreview(false);
|
setShowPreview(false);
|
||||||
@@ -36,6 +46,14 @@ export function FileEditor({ content, onChange, readOnly, filePath }: FileEditor
|
|||||||
setWordWrap((prev) => !prev);
|
setWordWrap((prev) => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleTextareaScroll = useCallback((event: UIEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (!lineNumbersRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineNumbersRef.current.scrollTop = event.currentTarget.scrollTop;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="file-editor-container">
|
<div className="file-editor-container">
|
||||||
{isMarkdown ? (
|
{isMarkdown ? (
|
||||||
@@ -96,14 +114,26 @@ export function FileEditor({ content, onChange, readOnly, filePath }: FileEditor
|
|||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<textarea
|
<div className={`file-editor-textarea-shell ${shouldRenderLineNumbers ? "file-editor-textarea-shell--line-numbers" : ""}`}>
|
||||||
className={`file-editor-textarea ${wordWrap ? "file-editor-textarea--wrap" : ""}`}
|
{shouldRenderLineNumbers && (
|
||||||
value={content}
|
<div className="file-editor-line-numbers" ref={lineNumbersRef} aria-hidden="true">
|
||||||
onChange={(e) => onChange(e.target.value)}
|
{Array.from({ length: lineCount }, (_, index) => (
|
||||||
readOnly={readOnly}
|
<div key={`line-${index + 1}`} className="file-editor-line-number">
|
||||||
spellCheck={false}
|
{index + 1}
|
||||||
aria-label={filePath ? `Editor for ${filePath}` : "File editor"}
|
</div>
|
||||||
/>
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<textarea
|
||||||
|
className={`file-editor-textarea ${wordWrap ? "file-editor-textarea--wrap" : ""}`}
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
onScroll={handleTextareaScroll}
|
||||||
|
readOnly={readOnly}
|
||||||
|
spellCheck={false}
|
||||||
|
aria-label={filePath ? `Editor for ${filePath}` : "File editor"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -723,6 +723,85 @@ describe("FileBrowserModal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("line number toggle", () => {
|
||||||
|
it("renders a header toggle and persists preference per project", async () => {
|
||||||
|
render(
|
||||||
|
<FileBrowserModal
|
||||||
|
initialWorkspace="project"
|
||||||
|
isOpen={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
projectId="proj-1"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("button", { name: /toggle line numbers/i });
|
||||||
|
expect(toggle).toHaveAttribute("aria-pressed", "false");
|
||||||
|
|
||||||
|
fireEvent.click(toggle);
|
||||||
|
expect(toggle).toHaveAttribute("aria-pressed", "true");
|
||||||
|
expect(localStorage.getItem("kb:proj-1:kb-files-line-numbers")).toBe("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads persisted preference when project changes", () => {
|
||||||
|
localStorage.setItem("kb:proj-a:kb-files-line-numbers", "true");
|
||||||
|
localStorage.setItem("kb:proj-b:kb-files-line-numbers", "false");
|
||||||
|
|
||||||
|
const { rerender } = render(
|
||||||
|
<FileBrowserModal
|
||||||
|
initialWorkspace="project"
|
||||||
|
isOpen={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
projectId="proj-a"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: /toggle line numbers/i })).toHaveAttribute("aria-pressed", "true");
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<FileBrowserModal
|
||||||
|
initialWorkspace="project"
|
||||||
|
isOpen={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
projectId="proj-b"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: /toggle line numbers/i })).toHaveAttribute("aria-pressed", "false");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only shows gutter for editable text files", async () => {
|
||||||
|
mockUseWorkspaceFileBrowser.mockReturnValue({
|
||||||
|
...defaultBrowserState,
|
||||||
|
entries: [
|
||||||
|
{ name: "editable.ts", type: "file" as const, size: 64, mtime: "2024-01-01" },
|
||||||
|
{ name: "readme.pdf", type: "file" as const, size: 64, mtime: "2024-01-01" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<FileBrowserModal
|
||||||
|
initialWorkspace="project"
|
||||||
|
isOpen={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /toggle line numbers/i }));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByText("editable.ts"));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.querySelector(".file-editor-line-numbers")).toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByText("readme.pdf"));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.querySelector(".file-editor-line-numbers")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("modal height constraint regression", () => {
|
describe("modal height constraint regression", () => {
|
||||||
it("max-height uses calc() to stay within viewport padding", async () => {
|
it("max-height uses calc() to stay within viewport padding", async () => {
|
||||||
const { loadAllAppCss } = await import("../../test/cssFixture");
|
const { loadAllAppCss } = await import("../../test/cssFixture");
|
||||||
|
|||||||
@@ -242,6 +242,42 @@ describe("FileEditor", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("line numbers", () => {
|
||||||
|
it("shows line numbers for editable text mode when enabled", () => {
|
||||||
|
render(
|
||||||
|
<FileEditor
|
||||||
|
content={"first\nsecond\nthird"}
|
||||||
|
onChange={vi.fn()}
|
||||||
|
filePath="src/app.ts"
|
||||||
|
showLineNumbers
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const gutter = document.querySelector(".file-editor-line-numbers");
|
||||||
|
expect(gutter).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("2")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("3")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides line numbers in markdown preview mode", () => {
|
||||||
|
render(
|
||||||
|
<FileEditor content="# Heading" onChange={vi.fn()} filePath="readme.md" showLineNumbers />,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /preview mode/i }));
|
||||||
|
expect(document.querySelector(".file-editor-line-numbers")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides line numbers for read-only files", () => {
|
||||||
|
render(
|
||||||
|
<FileEditor content={"one\ntwo"} onChange={vi.fn()} filePath="file.bin" readOnly showLineNumbers />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(document.querySelector(".file-editor-line-numbers")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("markdown preview scrollability", () => {
|
describe("markdown preview scrollability", () => {
|
||||||
it("preview container has correct CSS classes for scrolling", () => {
|
it("preview container has correct CSS classes for scrolling", () => {
|
||||||
render(<FileEditor content="# Hello World" onChange={vi.fn()} filePath="readme.md" />);
|
render(<FileEditor content="# Hello World" onChange={vi.fn()} filePath="readme.md" />);
|
||||||
|
|||||||
@@ -93,9 +93,10 @@ describe("projectStorage", () => {
|
|||||||
"kb-usage-modal-size",
|
"kb-usage-modal-size",
|
||||||
"kb-usage-provider-order",
|
"kb-usage-provider-order",
|
||||||
"kb-chat-active-session",
|
"kb-chat-active-session",
|
||||||
|
"kb-files-line-numbers",
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
expect(PROJECT_STORAGE_KEYS).toHaveLength(19);
|
expect(PROJECT_STORAGE_KEYS).toHaveLength(20);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has no overlap between global and project-scoped keys", () => {
|
it("has no overlap between global and project-scoped keys", () => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const PROJECT_STORAGE_KEYS: string[] = [
|
|||||||
"kb-usage-modal-size",
|
"kb-usage-modal-size",
|
||||||
"kb-usage-provider-order",
|
"kb-usage-provider-order",
|
||||||
"kb-chat-active-session",
|
"kb-chat-active-session",
|
||||||
|
"kb-files-line-numbers",
|
||||||
];
|
];
|
||||||
|
|
||||||
export function scopedKey(baseKey: string, projectId?: string): string {
|
export function scopedKey(baseKey: string, projectId?: string): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user