diff --git a/v2/src/lib/adapters/files-bridge.ts b/v2/src/lib/adapters/files-bridge.ts new file mode 100644 index 0000000..6d3b0ae --- /dev/null +++ b/v2/src/lib/adapters/files-bridge.ts @@ -0,0 +1,22 @@ +import { invoke } from '@tauri-apps/api/core'; + +export interface DirEntry { + name: string; + path: string; + is_dir: boolean; + size: number; + ext: string; +} + +export type FileContent = + | { type: 'Text'; content: string; lang: string } + | { type: 'Binary'; message: string } + | { type: 'TooLarge'; size: number }; + +export function listDirectoryChildren(path: string): Promise { + return invoke('list_directory_children', { path }); +} + +export function readFileContent(path: string): Promise { + return invoke('read_file_content', { path }); +} diff --git a/v2/src/lib/adapters/memory-adapter.ts b/v2/src/lib/adapters/memory-adapter.ts new file mode 100644 index 0000000..69d0e14 --- /dev/null +++ b/v2/src/lib/adapters/memory-adapter.ts @@ -0,0 +1,52 @@ +/** + * Pluggable memory adapter interface. + * Memora is the default implementation, but others can be swapped in. + */ + +export interface MemoryNode { + id: string | number; + content: string; + tags: string[]; + metadata?: Record; + created_at?: string; + updated_at?: string; +} + +export interface MemorySearchResult { + nodes: MemoryNode[]; + total: number; +} + +export interface MemoryAdapter { + readonly name: string; + readonly available: boolean; + + /** List memories, optionally filtered by tags */ + list(options?: { tags?: string[]; limit?: number; offset?: number }): Promise; + + /** Semantic search across memories */ + search(query: string, options?: { tags?: string[]; limit?: number }): Promise; + + /** Get a single memory by ID */ + get(id: string | number): Promise; +} + +/** Registry of available memory adapters */ +const adapters = new Map(); + +export function registerMemoryAdapter(adapter: MemoryAdapter): void { + adapters.set(adapter.name, adapter); +} + +export function getMemoryAdapter(name: string): MemoryAdapter | undefined { + return adapters.get(name); +} + +export function getAvailableAdapters(): MemoryAdapter[] { + return Array.from(adapters.values()).filter(a => a.available); +} + +export function getDefaultAdapter(): MemoryAdapter | undefined { + // Prefer Memora if available, otherwise first available + return adapters.get('memora') ?? getAvailableAdapters()[0]; +} diff --git a/v2/src/lib/components/Workspace/FilesTab.svelte b/v2/src/lib/components/Workspace/FilesTab.svelte new file mode 100644 index 0000000..716215f --- /dev/null +++ b/v2/src/lib/components/Workspace/FilesTab.svelte @@ -0,0 +1,384 @@ + + +
+ + +
+ {#if fileLoading} +
Loading…
+ {:else if !selectedPath} +
Select a file to view
+ {:else if fileContent?.type === 'TooLarge'} +
+ File too large + {formatSize(fileContent.size)} +
+ {:else if fileContent?.type === 'Binary'} + {#if isImageExt(selectedPath)} +
+ {selectedPath.split('/').pop()} +
+ {:else} +
{fileContent.message}
+ {/if} + {:else if fileContent?.type === 'Text'} +
+ {#if fileContent.lang === 'csv'} +
{fileContent.content}
+ {:else} + {@html renderHighlighted(fileContent.content, fileContent.lang)} + {/if} +
+ {/if} + {#if selectedPath} +
{selectedPath}
+ {/if} +
+
+ + diff --git a/v2/src/lib/components/Workspace/MemoriesTab.svelte b/v2/src/lib/components/Workspace/MemoriesTab.svelte new file mode 100644 index 0000000..1b7bab2 --- /dev/null +++ b/v2/src/lib/components/Workspace/MemoriesTab.svelte @@ -0,0 +1,375 @@ + + +
+ {#if !adapter} +
+
+ + + + +
+

No memory adapter configured

+

Register a memory adapter (e.g. Memora) to browse knowledge here.

+
+ {:else} +
+

{adapterName}

+ {total} memories +
+ {#each getAvailableAdapters() as a (a.name)} + + {/each} +
+
+ + + + {#if error} +
{error}
+ {/if} + +
+ {#if loading} +
Loading…
+ {:else if nodes.length === 0} +
No memories found
+ {:else} + {#each nodes as node (node.id)} + + {/each} + {/if} +
+ {/if} +
+ + diff --git a/v2/src/lib/components/Workspace/ProjectBox.svelte b/v2/src/lib/components/Workspace/ProjectBox.svelte index 032762e..3a48049 100644 --- a/v2/src/lib/components/Workspace/ProjectBox.svelte +++ b/v2/src/lib/components/Workspace/ProjectBox.svelte @@ -7,6 +7,9 @@ import TeamAgentsPanel from './TeamAgentsPanel.svelte'; import ProjectFiles from './ProjectFiles.svelte'; import ContextPane from '../Context/ContextPane.svelte'; + import FilesTab from './FilesTab.svelte'; + import SshTab from './SshTab.svelte'; + import MemoriesTab from './MemoriesTab.svelte'; import { getTerminalTabs } from '../../stores/workspace.svelte'; interface Props { @@ -22,12 +25,23 @@ let mainSessionId = $state(null); let terminalExpanded = $state(false); - type ProjectTab = 'claude' | 'files' | 'context'; - let activeTab = $state('claude'); + type ProjectTab = 'model' | 'docs' | 'context' | 'files' | 'ssh' | 'memories'; + let activeTab = $state('model'); + + // PERSISTED-LAZY: track which tabs have been activated at least once + let everActivated = $state>({}); let termTabs = $derived(getTerminalTabs(project.id)); let termTabCount = $derived(termTabs.length); + /** Activate a tab — for lazy tabs, mark as ever-activated */ + function switchTab(tab: ProjectTab) { + activeTab = tab; + if (!everActivated[tab]) { + everActivated = { ...everActivated, [tab]: true }; + } + } + function toggleTerminal() { terminalExpanded = !terminalExpanded; } @@ -48,38 +62,70 @@
+ class:active={activeTab === 'model'} + onclick={() => switchTab('model')} + >Model + class:active={activeTab === 'docs'} + onclick={() => switchTab('docs')} + >Docs + + +
- -
+ +
mainSessionId = id} /> {#if mainSessionId} {/if}
-
+
+ + + {#if everActivated['files']} +
+ +
+ {/if} + {#if everActivated['ssh']} +
+ +
+ {/if} + {#if everActivated['memories']} +
+ +
+ {/if}
-
+
+
+ + {#if showForm} +
+
{editing ? 'Edit Connection' : 'New Connection'}
+
+ + + + + + +
+
+ + +
+
+ {/if} + +
+ {#if loading} +
Loading…
+ {:else if sessions.length === 0 && !showForm} +
+

No SSH connections configured.

+

Add a connection to launch it as a terminal in the Model tab.

+
+ {:else} + {#each sessions as session (session.id)} +
+
+ {session.name} + {session.username}@{session.host}:{session.port} + {#if session.folder} + {session.folder} + {/if} +
+
+ + + +
+
+ {/each} + {/if} +
+
+ +