feat: add Files tab for browsing workspace files (#154)

* feat: add Files tab for browsing workspace files

- Add Files tab between Diff and Terminal in header
- Implement hierarchical file tree with expand/collapse
- Add fuzzy search with debouncing and relevance ranking
- Support gitignore filtering via `git check-ignore` (web + desktop)
- Add syntax highlighting for 150+ file types
- Add image preview (SVG, PNG, JPG, etc.)
- Add line numbers, wrap toggle, and copy button
- Desktop: split-pane layout matching DiffView
- Mobile: drill-in navigation with full-width sidebar
- Update help dialog with Cmd+3 shortcut
- Increase header breakpoint to 940px for new tab

* feat: enhance context and session stores to track agent/model/variant choices for historical sessions

* feat: implement line selection and commenting functionality in FilesView
This commit is contained in:
Bohdan Triapitsyn
2026-01-15 20:19:06 +02:00
committed by GitHub
parent ecf81c901d
commit 1be5dfda05
21 changed files with 2070 additions and 82 deletions
+2 -1
View File
@@ -5,7 +5,7 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip';
import { RiChat4Line, RiCodeLine, RiCommandLine, RiGitBranchLine, RiLayoutLeftLine, RiPlayListAddLine, RiQuestionLine, RiSettings3Line, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react';
import { RiChat4Line, RiCodeLine, RiCommandLine, RiFolder6Line, RiGitBranchLine, RiLayoutLeftLine, RiPlayListAddLine, RiQuestionLine, RiSettings3Line, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react';
import { useUIStore, type MainTab } from '@/stores/useUIStore';
import { useUpdateStore } from '@/stores/useUpdateStore';
import { useConfigStore } from '@/stores/useConfigStore';
@@ -266,6 +266,7 @@ export const Header: React.FC = () => {
icon: RiCodeLine,
badge: !isMobile && diffFileCount > 0 ? diffFileCount : undefined,
},
{ id: 'files', label: 'Files', icon: RiFolder6Line },
{ id: 'terminal', label: 'Terminal', icon: RiTerminalBoxLine },
{
id: 'git',
@@ -16,7 +16,7 @@ import { useDeviceInfo } from '@/lib/device';
import { useEdgeSwipe } from '@/hooks/useEdgeSwipe';
import { cn } from '@/lib/utils';
import { ChatView, GitView, DiffView, TerminalView, SettingsView } from '@/components/views';
import { ChatView, GitView, DiffView, TerminalView, FilesView, SettingsView } from '@/components/views';
export const MainLayout: React.FC = () => {
const {
@@ -306,6 +306,8 @@ export const MainLayout: React.FC = () => {
return <DiffView />;
case 'terminal':
return <TerminalView />;
case 'files':
return <FilesView />;
default:
return null;
}
+7 -1
View File
@@ -17,6 +17,7 @@ import {
RiCloseCircleLine,
RiCodeLine,
RiCommandLine,
RiFolder6Line,
RiGitBranchLine,
RiLayoutLeftLine,
RiPaletteLine,
@@ -161,11 +162,16 @@ export const HelpDialog: React.FC = () => {
},
{
keys: [`${mod} + 3`],
description: "Open Files",
icon: RiFolder6Line,
},
{
keys: [`${mod} + 4`],
description: "Open Terminal",
icon: RiTerminalBoxLine,
},
{
keys: [`${mod} + 4`],
keys: [`${mod} + 5`],
description: "Open Git Panel",
icon: RiGitBranchLine,
},
+4 -2
View File
@@ -9,16 +9,18 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
'peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground/30',
className
)}
style={{ width: '36px', height: '20px', minWidth: '36px', minHeight: '20px' }}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
'pointer-events-none block rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
)}
style={{ width: '16px', height: '16px', minWidth: '16px', minHeight: '16px' }}
/>
</SwitchPrimitives.Root>
));
File diff suppressed because it is too large Load Diff
@@ -9,9 +9,11 @@ import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { ensureFlexokiThemesRegistered } from '@/lib/shiki/registerFlexokiThemes';
import { flexokiThemeNames } from '@/lib/shiki/flexokiThemes';
import { toast } from 'sonner';
import { Textarea } from '@/components/ui/textarea';
import { useSessionStore } from '@/stores/useSessionStore';
import { useConfigStore } from '@/stores/useConfigStore';
import { useContextStore } from '@/stores/contextStore';
import { useUIStore } from '@/stores/useUIStore';
import { useDeviceInfo } from '@/lib/device';
import { cn, getModifierLabel } from '@/lib/utils';
@@ -158,6 +160,9 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
const sendMessage = useSessionStore(state => state.sendMessage);
const currentSessionId = useSessionStore(state => state.currentSessionId);
const { currentProviderId, currentModelId, currentAgentName, currentVariant } = useConfigStore();
const getSessionAgentSelection = useContextStore(state => state.getSessionAgentSelection);
const getAgentModelForSession = useContextStore(state => state.getAgentModelForSession);
const getAgentModelVariantForSession = useContextStore(state => state.getAgentModelVariantForSession);
// Update main content center on resize
useEffect(() => {
@@ -210,6 +215,9 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
// Check if click is inside the comment UI portal
const commentUI = document.querySelector('[data-comment-ui]');
if (commentUI?.contains(target)) return;
// Check if click is inside toast (sonner)
if (target.closest('[data-sonner-toast]') || target.closest('[data-sonner-toaster]')) return;
// Check if click is on a line number (inside shadow DOM)
const path = e.composedPath();
@@ -239,10 +247,25 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
const handleSendComment = useCallback(async () => {
if (!selection || !commentText.trim()) return;
if (!currentSessionId || !currentProviderId || !currentModelId) {
console.warn('Cannot send comment: no active session or model not selected');
if (!currentSessionId) {
toast.error('Select a session to send comment');
return;
}
// Get session-specific agent/model/variant with fallback to config values
const sessionAgent = getSessionAgentSelection(currentSessionId) || currentAgentName;
const sessionModel = sessionAgent ? getAgentModelForSession(currentSessionId, sessionAgent) : null;
const effectiveProviderId = sessionModel?.providerId || currentProviderId;
const effectiveModelId = sessionModel?.modelId || currentModelId;
if (!effectiveProviderId || !effectiveModelId) {
toast.error('Select a model to send comment');
return;
}
const effectiveVariant = sessionAgent && effectiveProviderId && effectiveModelId
? getAgentModelVariantForSession(currentSessionId, sessionAgent, effectiveProviderId, effectiveModelId) ?? currentVariant
: currentVariant;
const code = extractSelectedCode(original, modified, selection);
const startLine = selection.start;
@@ -259,18 +282,18 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
try {
await sendMessage(
message,
currentProviderId,
currentModelId,
currentAgentName,
effectiveProviderId,
effectiveModelId,
sessionAgent,
undefined,
undefined,
undefined,
currentVariant
effectiveVariant
);
} catch (e) {
console.error("Failed to send comment", e);
}
}, [selection, commentText, original, modified, fileName, language, sendMessage, currentSessionId, currentProviderId, currentModelId, currentAgentName, currentVariant, setActiveMainTab]);
}, [selection, commentText, original, modified, fileName, language, sendMessage, currentSessionId, currentProviderId, currentModelId, currentAgentName, currentVariant, setActiveMainTab, getSessionAgentSelection, getAgentModelForSession, getAgentModelVariantForSession]);
ensureFlexokiThemesRegistered();
@@ -339,7 +362,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
return (
<div
className="flex flex-col items-center gap-2 px-4"
style={{ width: 'min(100vw - 2rem, 42rem)' }}
style={{ width: 'min(100vw - 1rem, 42rem)' }}
>
<div className="w-full rounded-xl border bg-sidebar flex flex-col relative shadow-lg" style={{ borderColor: 'var(--primary)' }}>
{/* Textarea - auto-grows from 1 line to max 5 lines */}
@@ -45,7 +45,7 @@ const SETTINGS_SIDEBAR_MAX_WIDTH = 500;
const SETTINGS_SIDEBAR_DEFAULT_WIDTH = 264;
// Width threshold for hiding tab labels (show icons only)
const TAB_LABELS_MIN_WIDTH = 700;
const TAB_LABELS_MIN_WIDTH = 940;
interface SettingsViewProps {
onClose?: () => void;
@@ -2,4 +2,5 @@ export { ChatView } from './ChatView';
export { GitView } from './GitView';
export { DiffView, useDiffFileCount } from './DiffView';
export { TerminalView } from './TerminalView';
export { FilesView } from './FilesView';
export { SettingsView } from './SettingsView';
+1 -1
View File
@@ -920,7 +920,7 @@ html:not(.dark) .chat-scroll {
display: inline;
}
@media (max-width: 820px) {
@media (max-width: 940px) {
:root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-label {
display: none;
}
+6 -1
View File
@@ -339,11 +339,16 @@ export interface CommandExecResult {
error?: string;
}
export interface ListDirectoryOptions {
respectGitignore?: boolean;
}
export interface FilesAPI {
listDirectory(path: string): Promise<DirectoryListResult>;
listDirectory(path: string, options?: ListDirectoryOptions): Promise<DirectoryListResult>;
search(payload: FileSearchQuery): Promise<FileSearchResult[]>;
createDirectory(path: string): Promise<{ success: boolean; path: string }>;
readFile?(path: string): Promise<{ content: string; path: string }>;
readFileBinary?(path: string): Promise<{ dataUrl: string; path: string }>;
writeFile?(path: string, content: string): Promise<{ success: boolean; path: string }>;
execCommands?(commands: string[], cwd: string): Promise<{ success: boolean; results: CommandExecResult[] }>;
}
+4 -1
View File
@@ -1703,7 +1703,7 @@ class OpencodeService {
return result;
}
async listLocalDirectory(directoryPath: string | null | undefined): Promise<FilesystemEntry[]> {
async listLocalDirectory(directoryPath: string | null | undefined, options?: { respectGitignore?: boolean }): Promise<FilesystemEntry[]> {
const desktopFiles = getDesktopFilesApi();
if (desktopFiles) {
try {
@@ -1729,6 +1729,9 @@ class OpencodeService {
if (directoryPath && directoryPath.trim().length > 0) {
params.set('path', directoryPath);
}
if (options?.respectGitignore) {
params.set('respectGitignore', 'true');
}
const query = params.toString();
const response = await fetch(`${this.baseUrl}/fs/list${query ? `?${query}` : ''}`);
if (!response.ok) {
+357 -20
View File
@@ -214,72 +214,409 @@ export function detectToolOutputLanguage(
export function getLanguageFromExtension(filePath: string): string | null {
const ext = filePath.split('.').pop()?.toLowerCase();
// Handle special filenames without extensions
const filename = filePath.split('/').pop()?.toLowerCase() || '';
const filenameMap: Record<string, string> = {
'dockerfile': 'dockerfile',
'makefile': 'makefile',
'gnumakefile': 'makefile',
'cmakelists.txt': 'cmake',
'gemfile': 'ruby',
'rakefile': 'ruby',
'podfile': 'ruby',
'vagrantfile': 'ruby',
'guardfile': 'ruby',
'brewfile': 'ruby',
'fastfile': 'ruby',
'appfile': 'ruby',
'matchfile': 'ruby',
'pluginfile': 'ruby',
'scanfile': 'ruby',
'snapfile': 'ruby',
'.gitignore': 'text',
'.gitattributes': 'text',
'.gitmodules': 'ini',
'.editorconfig': 'ini',
'.npmrc': 'ini',
'.yarnrc': 'yaml',
'.prettierrc': 'json',
'.eslintrc': 'json',
'.babelrc': 'json',
'.browserslistrc': 'text',
'tsconfig.json': 'jsonc',
'jsconfig.json': 'jsonc',
'.env': 'bash',
'.env.local': 'bash',
'.env.development': 'bash',
'.env.production': 'bash',
'.env.test': 'bash',
'procfile': 'yaml',
'codeowners': 'text',
// Lock files
'package-lock.json': 'json',
'composer.lock': 'json',
'yarn.lock': 'yaml',
'pnpm-lock.yaml': 'yaml',
'cargo.lock': 'toml',
'poetry.lock': 'toml',
'gemfile.lock': 'ruby',
'pubspec.lock': 'yaml',
'packages.lock.json': 'json',
'bun.lockb': 'text',
'bun.lock': 'json',
};
if (filenameMap[filename]) {
return filenameMap[filename];
}
const languageMap: Record<string, string> = {
// JavaScript/TypeScript
'js': 'javascript',
'jsx': 'jsx',
'ts': 'typescript',
'tsx': 'tsx',
'mjs': 'javascript',
'cjs': 'javascript',
'mts': 'typescript',
'cts': 'typescript',
// Web markup/styling
'html': 'html',
'htm': 'html',
'xhtml': 'html',
'vue': 'html',
'svelte': 'html',
'astro': 'html',
'ejs': 'html',
'hbs': 'handlebars',
'handlebars': 'handlebars',
'mustache': 'handlebars',
'njk': 'twig',
'nunjucks': 'twig',
'twig': 'twig',
'liquid': 'liquid',
'css': 'css',
'scss': 'scss',
'sass': 'sass',
'less': 'less',
'styl': 'stylus',
'stylus': 'stylus',
'pcss': 'css',
'postcss': 'css',
// Data/config formats
'json': 'json',
'jsonc': 'json',
'json5': 'json',
'jsonl': 'json',
'ndjson': 'json',
'geojson': 'json',
'yaml': 'yaml',
'yml': 'yaml',
'toml': 'toml',
'xml': 'xml',
'xsl': 'xml',
'xslt': 'xml',
'xsd': 'xml',
'dtd': 'xml',
'plist': 'xml',
'svg': 'xml',
'rss': 'xml',
'atom': 'xml',
'xaml': 'xml',
'csproj': 'xml',
'vbproj': 'xml',
'fsproj': 'xml',
'props': 'xml',
'targets': 'xml',
'nuspec': 'xml',
'resx': 'xml',
'ini': 'ini',
'cfg': 'ini',
'conf': 'ini',
'config': 'ini',
'properties': 'properties',
'env': 'bash',
'csv': 'text',
'tsv': 'text',
// Python
'py': 'python',
'pyw': 'python',
'pyx': 'python',
'pxd': 'python',
'pxi': 'python',
'pyi': 'python',
'gyp': 'python',
'gypi': 'python',
'bzl': 'python',
// Ruby
'rb': 'ruby',
'go': 'go',
'rs': 'rust',
'erb': 'erb',
'rake': 'ruby',
'gemspec': 'ruby',
'ru': 'ruby',
'podspec': 'ruby',
'thor': 'ruby',
'jbuilder': 'ruby',
'rabl': 'ruby',
'builder': 'ruby',
// PHP
'php': 'php',
'phtml': 'php',
'php3': 'php',
'php4': 'php',
'php5': 'php',
'php7': 'php',
'phps': 'php',
'inc': 'php',
'blade.php': 'php',
// Java/JVM
'java': 'java',
'kt': 'kotlin',
'swift': 'swift',
'kts': 'kotlin',
'scala': 'scala',
'sc': 'scala',
'groovy': 'groovy',
'gradle': 'groovy',
'gvy': 'groovy',
'gy': 'groovy',
'gsh': 'groovy',
// C/C++/Objective-C
'c': 'c',
'h': 'c',
'cpp': 'cpp',
'cc': 'cpp',
'h': 'c',
'cxx': 'cpp',
'c++': 'cpp',
'hpp': 'cpp',
'cs': 'csharp',
'php': 'php',
'dart': 'dart',
'r': 'r',
'lua': 'lua',
'vim': 'vim',
'hxx': 'cpp',
'hh': 'cpp',
'h++': 'cpp',
'ino': 'cpp',
'm': 'objectivec',
'mm': 'objectivec',
// C#/F#/.NET
'cs': 'csharp',
'csx': 'csharp',
'cake': 'csharp',
'fs': 'fsharp',
'fsx': 'fsharp',
'fsi': 'fsharp',
'vb': 'vbnet',
// Go
'go': 'go',
'mod': 'go',
'sum': 'text',
// Rust
'rs': 'rust',
// Swift
'swift': 'swift',
// Dart
'dart': 'dart',
// Lua
'lua': 'lua',
// Perl
'pl': 'perl',
'pm': 'perl',
'pod': 'perl',
't': 'perl',
// R
'r': 'r',
'R': 'r',
'rmd': 'markdown',
'rnw': 'r',
// Julia
'jl': 'julia',
// Haskell
'hs': 'haskell',
'lhs': 'haskell',
// Elixir/Erlang
'ex': 'elixir',
'exs': 'elixir',
'eex': 'elixir',
'heex': 'elixir',
'leex': 'elixir',
'erl': 'erlang',
'hrl': 'erlang',
// Clojure
'clj': 'clojure',
'cljs': 'clojure',
'cljc': 'clojure',
'edn': 'clojure',
// Lisp/Scheme
'lisp': 'lisp',
'cl': 'lisp',
'el': 'lisp',
'scm': 'scheme',
'ss': 'scheme',
'rkt': 'scheme',
// OCaml/ReasonML
'ml': 'ocaml',
'mli': 'ocaml',
're': 'reason',
'rei': 'reason',
// Nim
'nim': 'nim',
'nims': 'nim',
'nimble': 'nim',
// Zig
'zig': 'zig',
// V
'v': 'v',
'vsh': 'v',
// Crystal
'cr': 'crystal',
// D
'd': 'd',
'di': 'd',
// Shell/Scripts
'sh': 'bash',
'bash': 'bash',
'zsh': 'bash',
'fish': 'bash',
'ksh': 'bash',
'csh': 'bash',
'tcsh': 'bash',
'ps1': 'powershell',
'psm1': 'powershell',
'psd1': 'powershell',
'bat': 'batch',
'cmd': 'batch',
// SQL
'sql': 'sql',
'psql': 'sql',
'plsql': 'sql',
'mysql': 'sql',
'pgsql': 'sql',
'sqlite': 'sql',
// GraphQL
'graphql': 'graphql',
'gql': 'graphql',
// Solidity
'sol': 'solidity',
// Assembly
'asm': 'nasm',
's': 'nasm',
'S': 'nasm',
// Nix
'nix': 'nix',
// Terraform/HCL
'tf': 'hcl',
'tfvars': 'hcl',
'hcl': 'hcl',
// Docker
'dockerignore': 'text',
// Puppet
'pp': 'puppet',
// LaTeX
'tex': 'latex',
'latex': 'latex',
'sty': 'latex',
'cls': 'latex',
'bib': 'bibtex',
'bst': 'bibtex',
// Markdown/docs
'md': 'markdown',
'mdx': 'markdown',
'markdown': 'markdown',
'mdown': 'markdown',
'mkd': 'markdown',
'rst': 'text',
'adoc': 'asciidoc',
'asciidoc': 'asciidoc',
'org': 'text',
'txt': 'text',
'text': 'text',
'rtf': 'text',
'dockerfile': 'dockerfile',
'makefile': 'makefile',
'gitignore': 'text',
'env': 'text',
'conf': 'text',
'cfg': 'text',
'ini': 'ini',
// Vim
'vim': 'vim',
'vimrc': 'vim',
'sql': 'sql',
// Makefile variants
'mk': 'makefile',
// CMake
'cmake': 'cmake',
// Diff/Patch
'diff': 'diff',
'patch': 'diff'
'patch': 'diff',
// Prisma
'prisma': 'prisma',
// Protocol Buffers
'proto': 'protobuf',
// Thrift
'thrift': 'thrift',
// WASM
'wat': 'wasm',
'wast': 'wasm',
// GLSL/Shaders
'glsl': 'glsl',
'vert': 'glsl',
'frag': 'glsl',
'geom': 'glsl',
'comp': 'glsl',
'hlsl': 'hlsl',
'fx': 'hlsl',
'cg': 'cg',
'shader': 'glsl',
// Apache/Nginx config
'htaccess': 'apacheconf',
'nginx': 'nginx',
// Kubernetes
'kubeconfig': 'yaml',
// Ansible
'ansible': 'yaml',
};
return languageMap[ext || ''] || null;
+26 -6
View File
@@ -265,21 +265,37 @@ export const useContextStore = create<ContextStore>()(
const allMessages = sessionMessages.filter((m: any) => m.info.role === "assistant" || m.info.role === "user").sort((a: any, b: any) => a.info.time.created - b.info.time.created);
const assistantMessages = sessionMessages.filter((m: any) => m.info.role === "assistant").sort((a: any, b: any) => a.info.time.created - b.info.time.created);
// Track variant from user messages to apply to corresponding assistant response
let pendingVariant: string | undefined = undefined;
let pendingUserModel: { providerID: string; modelID: string } | undefined = undefined;
for (let messageIndex = 0; messageIndex < allMessages.length; messageIndex++) {
const message = allMessages[messageIndex];
const { info } = message;
const infoAny = info as any;
// User messages have variant and model info in different structure
if (infoAny.role === "user") {
// User message: variant is top-level, model is nested in model.providerID/modelID
pendingVariant = typeof infoAny.variant === 'string' && infoAny.variant.trim().length > 0
? infoAny.variant
: undefined;
pendingUserModel = infoAny.model?.providerID && infoAny.model?.modelID
? { providerID: infoAny.model.providerID, modelID: infoAny.model.modelID }
: undefined;
continue;
}
// Assistant message: providerID/modelID are top-level
if (infoAny.providerID && infoAny.modelID) {
const agentName = extractAgentFromMessage(infoAny, assistantMessages.indexOf(message));
if (agentName && agents.find((a) => a.name === agentName)) {
const resolvedVariant = typeof infoAny.variant === 'string' && infoAny.variant.trim().length > 0
? infoAny.variant
: undefined;
if (resolvedVariant) {
saveAgentModelVariantForSession(sessionId, agentName, infoAny.providerID, infoAny.modelID, resolvedVariant);
// Apply pending variant from user message if model matches
if (pendingVariant && pendingUserModel &&
pendingUserModel.providerID === infoAny.providerID &&
pendingUserModel.modelID === infoAny.modelID) {
saveAgentModelVariantForSession(sessionId, agentName, infoAny.providerID, infoAny.modelID, pendingVariant);
}
const choice = {
@@ -294,6 +310,10 @@ export const useContextStore = create<ContextStore>()(
}
}
}
// Clear pending variant after processing assistant message
pendingVariant = undefined;
pendingUserModel = undefined;
}
for (const [agentName, choice] of agentLastChoices) {
+18
View File
@@ -288,6 +288,24 @@ export const useSessionStore = create<SessionStore>()(
}
get().trimToViewportWindow(id, ACTIVE_SESSION_WINDOW);
// Analyze session messages to extract agent/model/variant choices
// This ensures context is available even when ModelControls isn't mounted
const sessionMessages = get().messages.get(id);
if (sessionMessages && sessionMessages.length > 0) {
const agents = useConfigStore.getState().agents;
if (agents.length > 0) {
try {
await useContextStore.getState().analyzeAndSaveExternalSessionChoices(
id,
agents,
get().messages
);
} catch (error) {
console.warn('Failed to analyze session choices:', error);
}
}
}
}
get().evictLeastRecentlyUsed();
+1 -1
View File
@@ -4,7 +4,7 @@ import type { SidebarSection } from '@/constants/sidebar';
import { getSafeStorage } from './utils/safeStorage';
import { SEMANTIC_TYPOGRAPHY, getTypographyVariable, type SemanticTypographyKey } from '@/lib/typography';
export type MainTab = 'chat' | 'git' | 'diff' | 'terminal';
export type MainTab = 'chat' | 'git' | 'diff' | 'terminal' | 'files';
export type EventStreamStatus =
| 'idle'
| 'connecting'
@@ -0,0 +1,14 @@
declare module 'react-syntax-highlighter/create-element' {
import type { ReactNode } from 'react';
type CreateElementOptions = {
node: unknown;
stylesheet: unknown;
useInlineStyles: boolean;
key?: string | number;
};
const createElement: (options: CreateElementOptions) => ReactNode;
export default createElement;
}