refactor(v3): project-scoped ContextPane with auto-registration
This commit is contained in:
parent
0f0ea3fb59
commit
e37c85e294
5 changed files with 139 additions and 147 deletions
|
|
@ -141,6 +141,24 @@ impl CtxDb {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Register a project in the ctx database (creates if not exists).
|
||||
pub fn register_project(&self, name: &str, description: &str, work_dir: Option<&str>) -> Result<(), String> {
|
||||
let db_path = Self::db_path();
|
||||
if !db_path.exists() {
|
||||
return Err("ctx database not found".to_string());
|
||||
}
|
||||
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open database: {e}"))?;
|
||||
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO sessions (name, description, work_dir) VALUES (?1, ?2, ?3)",
|
||||
rusqlite::params![name, description, work_dir],
|
||||
).map_err(|e| format!("Failed to register project: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_projects(&self) -> Result<Vec<CtxProject>, String> {
|
||||
let lock = self.conn.lock().unwrap();
|
||||
let conn = lock.as_ref().ok_or("ctx database not found")?;
|
||||
|
|
|
|||
|
|
@ -187,6 +187,11 @@ fn ctx_init_db(state: State<'_, AppState>) -> Result<(), String> {
|
|||
state.ctx_db.init_db()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn ctx_register_project(state: State<'_, AppState>, name: String, description: String, work_dir: Option<String>) -> Result<(), String> {
|
||||
state.ctx_db.register_project(&name, &description, work_dir.as_deref())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn ctx_list_projects(state: State<'_, AppState>) -> Result<Vec<ctx::CtxProject>, String> {
|
||||
state.ctx_db.list_projects()
|
||||
|
|
@ -545,6 +550,7 @@ pub fn run() {
|
|||
ssh_session_save,
|
||||
ssh_session_delete,
|
||||
ctx_init_db,
|
||||
ctx_register_project,
|
||||
ctx_list_projects,
|
||||
ctx_get_context,
|
||||
ctx_get_shared,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ export async function ctxInitDb(): Promise<void> {
|
|||
return invoke('ctx_init_db');
|
||||
}
|
||||
|
||||
export async function ctxRegisterProject(name: string, description: string, workDir?: string): Promise<void> {
|
||||
return invoke('ctx_register_project', { name, description, workDir: workDir ?? null });
|
||||
}
|
||||
|
||||
export async function ctxListProjects(): Promise<CtxProject[]> {
|
||||
return invoke('ctx_list_projects');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,24 +2,22 @@
|
|||
import { onMount } from 'svelte';
|
||||
import {
|
||||
ctxInitDb,
|
||||
ctxListProjects,
|
||||
ctxRegisterProject,
|
||||
ctxGetContext,
|
||||
ctxGetShared,
|
||||
ctxGetSummaries,
|
||||
ctxSearch,
|
||||
type CtxProject,
|
||||
type CtxEntry,
|
||||
type CtxSummary,
|
||||
} from '../../adapters/ctx-bridge';
|
||||
|
||||
interface Props {
|
||||
onExit?: () => void;
|
||||
projectName: string;
|
||||
projectCwd: string;
|
||||
}
|
||||
|
||||
let { onExit }: Props = $props();
|
||||
let { projectName, projectCwd }: Props = $props();
|
||||
|
||||
let projects = $state<CtxProject[]>([]);
|
||||
let selectedProject = $state<string | null>(null);
|
||||
let entries = $state<CtxEntry[]>([]);
|
||||
let sharedEntries = $state<CtxEntry[]>([]);
|
||||
let summaries = $state<CtxSummary[]>([]);
|
||||
|
|
@ -30,15 +28,27 @@
|
|||
let dbMissing = $state(false);
|
||||
let initializing = $state(false);
|
||||
|
||||
async function loadData() {
|
||||
async function loadProjectContext() {
|
||||
loading = true;
|
||||
try {
|
||||
projects = await ctxListProjects();
|
||||
sharedEntries = await ctxGetShared();
|
||||
// Register project if not already (INSERT OR IGNORE)
|
||||
await ctxRegisterProject(projectName, `BTerminal project: ${projectName}`, projectCwd);
|
||||
|
||||
const [ctx, shared, sums] = await Promise.all([
|
||||
ctxGetContext(projectName),
|
||||
ctxGetShared(),
|
||||
ctxGetSummaries(projectName, 5),
|
||||
]);
|
||||
entries = ctx;
|
||||
sharedEntries = shared;
|
||||
summaries = sums;
|
||||
error = '';
|
||||
dbMissing = false;
|
||||
} catch (e) {
|
||||
error = `${e}`;
|
||||
dbMissing = error.includes('not found');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +56,7 @@
|
|||
initializing = true;
|
||||
try {
|
||||
await ctxInitDb();
|
||||
await loadData();
|
||||
await loadProjectContext();
|
||||
} catch (e) {
|
||||
error = `Failed to initialize database: ${e}`;
|
||||
} finally {
|
||||
|
|
@ -54,24 +64,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
onMount(loadData);
|
||||
|
||||
async function selectProject(name: string) {
|
||||
selectedProject = name;
|
||||
loading = true;
|
||||
try {
|
||||
[entries, summaries] = await Promise.all([
|
||||
ctxGetContext(name),
|
||||
ctxGetSummaries(name, 5),
|
||||
]);
|
||||
error = '';
|
||||
} catch (e) {
|
||||
error = `Failed to load context: ${e}`;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
if (!searchQuery.trim()) {
|
||||
searchResults = [];
|
||||
|
|
@ -83,11 +75,13 @@
|
|||
error = `Search failed: ${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(loadProjectContext);
|
||||
</script>
|
||||
|
||||
<div class="context-pane">
|
||||
<div class="ctx-header">
|
||||
<h3>Context Manager</h3>
|
||||
<h3>{projectName}</h3>
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
|
|
@ -124,69 +118,53 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="ctx-body">
|
||||
{#if searchResults.length > 0}
|
||||
<div class="section">
|
||||
<h4>Search Results</h4>
|
||||
{#each searchResults as result}
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="entry-project">{result.project}</span>
|
||||
<span class="entry-key">{result.key}</span>
|
||||
{#if !error}
|
||||
<div class="ctx-body">
|
||||
{#if loading}
|
||||
<div class="loading">Loading...</div>
|
||||
{:else if searchResults.length > 0}
|
||||
<div class="section">
|
||||
<h4>Search Results</h4>
|
||||
{#each searchResults as result}
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="entry-project">{result.project}</span>
|
||||
<span class="entry-key">{result.key}</span>
|
||||
</div>
|
||||
<pre class="entry-value">{result.value}</pre>
|
||||
</div>
|
||||
<pre class="entry-value">{result.value}</pre>
|
||||
{/each}
|
||||
<button class="clear-btn" onclick={() => { searchResults = []; searchQuery = ''; }}>Clear search</button>
|
||||
</div>
|
||||
{:else}
|
||||
{#if entries.length > 0}
|
||||
<div class="section">
|
||||
<h4>Project Context</h4>
|
||||
{#each entries as entry}
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="entry-key">{entry.key}</span>
|
||||
<span class="entry-date">{entry.updated_at}</span>
|
||||
</div>
|
||||
<pre class="entry-value">{entry.value}</pre>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
<button class="clear-btn" onclick={() => { searchResults = []; searchQuery = ''; }}>Clear search</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="project-list">
|
||||
<h4>Projects</h4>
|
||||
{#if projects.length === 0}
|
||||
<p class="empty">No projects registered. Use <code>ctx init</code> to add one.</p>
|
||||
{/if}
|
||||
{#each projects as project}
|
||||
<button
|
||||
class="project-btn"
|
||||
class:active={selectedProject === project.name}
|
||||
onclick={() => selectProject(project.name)}
|
||||
>
|
||||
<span class="project-name">{project.name}</span>
|
||||
<span class="project-desc">{project.description}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if sharedEntries.length > 0}
|
||||
<div class="section">
|
||||
<h4>Shared Context</h4>
|
||||
{#each sharedEntries as entry}
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="entry-key">{entry.key}</span>
|
||||
{#if sharedEntries.length > 0}
|
||||
<div class="section">
|
||||
<h4>Shared Context</h4>
|
||||
{#each sharedEntries as entry}
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="entry-key">{entry.key}</span>
|
||||
</div>
|
||||
<pre class="entry-value">{entry.value}</pre>
|
||||
</div>
|
||||
<pre class="entry-value">{entry.value}</pre>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedProject && !loading}
|
||||
<div class="section">
|
||||
<h4>{selectedProject} Context</h4>
|
||||
{#if entries.length === 0}
|
||||
<p class="empty">No context entries for this project.</p>
|
||||
{/if}
|
||||
{#each entries as entry}
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="entry-key">{entry.key}</span>
|
||||
<span class="entry-date">{entry.updated_at}</span>
|
||||
</div>
|
||||
<pre class="entry-value">{entry.value}</pre>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if summaries.length > 0}
|
||||
<div class="section">
|
||||
|
|
@ -201,13 +179,16 @@
|
|||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div class="loading">Loading...</div>
|
||||
{#if entries.length === 0 && sharedEntries.length === 0 && summaries.length === 0}
|
||||
<div class="empty-state">
|
||||
<p class="empty">No context stored yet.</p>
|
||||
<p class="empty">Use <code>ctx set {projectName} <key> <value></code> to add context entries.</p>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
@ -231,9 +212,10 @@
|
|||
}
|
||||
|
||||
.ctx-header h3 {
|
||||
font-size: 13px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
color: var(--ctp-blue);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
|
|
@ -312,10 +294,6 @@
|
|||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.project-list {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
|
|
@ -325,89 +303,75 @@
|
|||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.project-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
background: var(--ctp-surface0);
|
||||
border: 1px solid var(--ctp-surface0);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
color: var(--ctp-text);
|
||||
}
|
||||
|
||||
.project-btn:hover { border-color: var(--ctp-blue); }
|
||||
.project-btn.active {
|
||||
border-color: var(--ctp-blue);
|
||||
background: color-mix(in srgb, var(--ctp-blue) 10%, var(--ctp-surface0));
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.project-desc {
|
||||
font-size: 10px;
|
||||
color: var(--ctp-overlay0);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.entry {
|
||||
background: var(--ctp-surface0);
|
||||
border-radius: 0.25rem;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 4px;
|
||||
padding: 0.375rem 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.entry-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.entry-project {
|
||||
font-size: 10px;
|
||||
font-size: 0.625rem;
|
||||
color: var(--ctp-blue);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.entry-key {
|
||||
font-size: 11px;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
color: var(--ctp-green);
|
||||
}
|
||||
|
||||
.entry-date {
|
||||
font-size: 9px;
|
||||
font-size: 0.5625rem;
|
||||
color: var(--ctp-overlay0);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.entry-value {
|
||||
font-size: 11px;
|
||||
font-size: 0.6875rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--ctp-subtext0);
|
||||
max-height: 200px;
|
||||
max-height: 12.5rem;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--ctp-overlay0);
|
||||
font-size: 11px;
|
||||
font-size: 0.6875rem;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.empty code {
|
||||
background: var(--ctp-surface0);
|
||||
padding: 0.0625rem 0.3125rem;
|
||||
border-radius: 0.1875rem;
|
||||
font-family: var(--term-font-family, monospace);
|
||||
color: var(--ctp-green);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
|
|
@ -415,18 +379,18 @@
|
|||
border: 1px solid var(--ctp-surface0);
|
||||
color: var(--ctp-subtext0);
|
||||
border-radius: 0.25rem;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
padding: 0.25rem 0.625rem;
|
||||
font-size: 0.6875rem;
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.clear-btn:hover { color: var(--ctp-text); }
|
||||
|
||||
.loading {
|
||||
color: var(--ctp-overlay0);
|
||||
font-size: 12px;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@
|
|||
</div>
|
||||
{:else if activeTab === 'context'}
|
||||
<div class="content-pane">
|
||||
<ContextPane onExit={() => {}} />
|
||||
<ContextPane projectName={project.name} projectCwd={project.cwd} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue