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
+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'