Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.glyphformac.com/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Glyph uses React Context for all global state management. No Redux, no Zustand—just plain React.
Contexts are layered: SpaceContext wraps FileTreeContext wraps ViewContext, etc.

Context Hierarchy

src/main.tsx
function App() {
  return (
    <SpaceProvider>           {/* Space lifecycle */}
      <FileTreeProvider>      {/* Files, tags, active file */}
        <ViewProvider>        {/* Active view document */}
          <UIProvider>        {/* Sidebar, search state */}
            <EditorProvider>  {/* TipTap editor instance */}
              <AppShell />
            </EditorProvider>
          </UIProvider>
        </ViewProvider>
      </FileTreeProvider>
    </SpaceProvider>
  );
}

SpaceContext

Location: src/contexts/SpaceContext.tsx Manages space lifecycle (create, open, close).

State Shape

interface SpaceContextValue {
  // App metadata
  info: AppInfo | null;             // App name, version
  
  // Current space
  spacePath: string | null;         // '/Users/me/my-space'
  spaceSchemaVersion: number | null; // 1
  
  // History
  lastSpacePath: string | null;     // For "Continue" button
  recentSpaces: string[];           // Up to 20 recent paths
  
  // Index state
  isIndexing: boolean;              // Index rebuild in progress
  
  // Lifecycle
  settingsLoaded: boolean;          // Settings loaded from disk
  error: string;                    // Error message
  setError: (error: string) => void;
  
  // Actions
  onOpenSpace: () => Promise<void>;
  onOpenSpaceAtPath: (path: string) => Promise<void>;
  onContinueLastSpace: () => Promise<void>;
  onCreateSpace: () => Promise<void>;
  closeSpace: () => Promise<void>;
  startIndexRebuild: () => Promise<void>;
}

Usage

import { useSpace } from '@/contexts/SpaceContext';

function WelcomeScreen() {
  const { onOpenSpace, onContinueLastSpace, lastSpacePath } = useSpace();
  
  return (
    <div>
      <Button onClick={onOpenSpace}>Open Space</Button>
      
      {lastSpacePath && (
        <Button onClick={onContinueLastSpace}>
          Continue: {lastSpacePath}
        </Button>
      )}
    </div>
  );
}

FileTreeContext

Location: src/contexts/FileTreeContext.tsx Manages file browser state and tag index.

State Shape

interface FileTreeContextValue {
  // File tree data
  rootEntries: FsEntry[];                    // Files/dirs at root
  childrenByDir: Record<string, FsEntry[]>;  // Cached children by dir path
  expandedDirs: Set<string>;                 // Which dirs are expanded
  
  // Updaters (for hooks to modify state)
  updateRootEntries: (next: FsEntry[] | ((prev: FsEntry[]) => FsEntry[])) => void;
  updateChildrenByDir: (next: ...) => void;
  updateExpandedDirs: (next: Set<string> | ((prev: Set<string>) => Set<string>)) => void;
  
  // Active file
  activeFilePath: string | null;   // 'notes/example.md'
  setActiveFilePath: (path: string | null) => void;
  
  // Derived state (computed from activeFilePath)
  activeNoteId: string | null;     // Same as activeFilePath if .md
  activeNoteTitle: string | null;  // Filename without extension
  
  // Tag index
  tags: TagCount[];                // [{ tag: 'research', count: 42 }]
  tagsError: string;
  refreshTags: () => Promise<void>;
}

Usage

import { useFileTreeContext } from '@/contexts/FileTreeContext';

function FileTreePane() {
  const { rootEntries, expandedDirs } = useFileTreeContext();
  
  return (
    <div>
      {rootEntries.map(entry => (
        <FileTreeItem
          key={entry.rel_path}
          entry={entry}
          isExpanded={expandedDirs.has(entry.rel_path)}
        />
      ))}
    </div>
  );
}

ViewContext

Location: src/contexts/ViewContext.tsx Manages “view documents” (folder, tag, search, database views).

State Shape

interface ViewContextValue {
  activeViewDoc: ViewDoc | null;  // Current view (folder, tag, etc.)
  setActiveViewDoc: (doc: ViewDoc | null) => void;
  
  isLoadingView: boolean;
  viewError: string;
}

type ViewDoc =
  | FolderViewDoc
  | TagViewDoc
  | SearchViewDoc
  | DatabaseViewDoc;

interface FolderViewDoc {
  type: 'folder';
  dir: string;
  files: FsEntry[];
  subfolders: FolderViewFolder[];
  note_previews: ViewNotePreview[];
}

Usage

import { useViewContext } from '@/contexts/ViewContext';

function MainContent() {
  const { activeViewDoc } = useViewContext();
  
  if (activeViewDoc?.type === 'folder') {
    return <FolderView doc={activeViewDoc} />;
  }
  
  if (activeViewDoc?.type === 'database') {
    return <DatabasePane doc={activeViewDoc} />;
  }
  
  return <MarkdownEditorPane />;
}

UIContext

Location: src/contexts/UIContext.tsx Manages UI chrome state (sidebar, search, modals).

State Shape

interface UIContextValue {
  // Sidebar
  activePane: 'files' | 'tags' | 'ai' | 'tasks';
  setActivePane: (pane: 'files' | 'tags' | 'ai' | 'tasks') => void;
  
  isSidebarCollapsed: boolean;
  toggleSidebar: () => void;
  
  // Command palette (Cmd+K)
  isCommandPaletteOpen: boolean;
  openCommandPalette: () => void;
  closeCommandPalette: () => void;
  
  // Search
  searchQuery: string;
  setSearchQuery: (query: string) => void;
  
  // Preview pane
  activePreviewPath: string | null;
  setActivePreviewPath: (path: string | null) => void;
}

Usage

function SidebarHeader() {
  const { activePane, setActivePane } = useUIContext();
  
  return (
    <div>
      <IconButton
        onClick={() => setActivePane('files')}
        data-active={activePane === 'files'}
      >
        <FileIcon />
      </IconButton>
      
      <IconButton
        onClick={() => setActivePane('ai')}
        data-active={activePane === 'ai'}
      >
        <SparkleIcon />
      </IconButton>
    </div>
  );
}

EditorContext

Location: src/contexts/EditorContext.tsx Manages TipTap editor instance.

State Shape

import type { Editor } from '@tiptap/react';

interface EditorContextValue {
  editor: Editor | null;              // TipTap editor instance
  setEditor: (editor: Editor | null) => void;
  
  isEditing: boolean;                 // Focus state
  saveState: 'saved' | 'saving' | 'unsaved' | 'error';
  setSaveState: (state: 'saved' | 'saving' | 'unsaved' | 'error') => void;
}

Usage

import { useEditor } from '@tiptap/react';
import { useEditorContext } from '@/contexts/EditorContext';

function MarkdownEditorPane() {
  const { setEditor, setSaveState } = useEditorContext();
  
  const editor = useEditor({
    extensions: [/* ... */],
    onUpdate: () => {
      setSaveState('unsaved');
      debouncedSave();
    },
    onFocus: () => setIsEditing(true),
    onBlur: () => setIsEditing(false),
  });
  
  useEffect(() => {
    setEditor(editor);
    return () => setEditor(null);
  }, [editor, setEditor]);
  
  return <EditorContent editor={editor} />;
}

Custom Context Pattern

All contexts follow this pattern:
1

Define context value type

interface MyContextValue {
  data: string;
  setData: (data: string) => void;
}
2

Create context with null default

const MyContext = createContext<MyContextValue | null>(null);
3

Create provider component

export function MyProvider({ children }: { children: ReactNode }) {
  const [data, setData] = useState('');
  
  const value = useMemo<MyContextValue>(
    () => ({ data, setData }),
    [data]
  );
  
  return (
    <MyContext.Provider value={value}>
      {children}
    </MyContext.Provider>
  );
}
4

Create typed hook

export function useMyContext(): MyContextValue {
  const ctx = useContext(MyContext);
  if (!ctx) {
    throw new Error('useMyContext must be used within MyProvider');
  }
  return ctx;
}

Performance Optimization

Split Contexts

Instead of one giant context:
Bad
interface AppContextValue {
  user: User;
  theme: Theme;
  files: File[];
  // ... 20 more fields
}

// Every component re-renders when ANY field changes!
Use multiple small contexts:
Good
<UserProvider>
  <ThemeProvider>
    <FileProvider>
      {/* Components only re-render when their context changes */}
    </FileProvider>
  </ThemeProvider>
</UserProvider>

Memoize Context Value

Always memoize the context value:
const value = useMemo<MyContextValue>(
  () => ({ data, setData }),
  [data] // Only recompute when data changes
);
Without useMemo, context consumers re-render on every provider render.

Selector Pattern

For large contexts, expose selectors:
interface FileTreeContextValue {
  // Instead of exposing entire state:
  // state: { rootEntries, childrenByDir, ... }
  
  // Expose specific selectors:
  useRootEntries: () => FsEntry[];
  useChildrenByDir: () => Record<string, FsEntry[]>;
  useExpandedDirs: () => Set<string>;
}

// Components only subscribe to what they use
function FileList() {
  const rootEntries = useFileTreeContext().useRootEntries();
  // Only re-renders when rootEntries changes
}

Next Steps

Hooks

Custom React hooks

Components

Component architecture