Skip to content

Commit 8710356

Browse files
committed
dashboard: add fact/note editing, compartment dates, experimental config, confirm dialogs
1 parent 0ba6684 commit 8710356

7 files changed

Lines changed: 305 additions & 76 deletions

File tree

packages/dashboard/src-tauri/src/commands.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,63 @@ pub fn get_smart_notes(
122122
db::get_smart_notes(&conn, &project_path).map_err(|e| e.to_string())
123123
}
124124

125+
#[tauri::command]
126+
pub fn update_session_fact(
127+
state: State<'_, AppState>,
128+
fact_id: i64,
129+
content: String,
130+
) -> Result<(), String> {
131+
let path = state.get_db_path()?;
132+
let conn = db::open_readwrite(&path).map_err(|e| e.to_string())?;
133+
db::update_session_fact(&conn, fact_id, &content).map_err(|e| e.to_string())?;
134+
Ok(())
135+
}
136+
137+
#[tauri::command]
138+
pub fn delete_session_fact(
139+
state: State<'_, AppState>,
140+
fact_id: i64,
141+
) -> Result<(), String> {
142+
let path = state.get_db_path()?;
143+
let conn = db::open_readwrite(&path).map_err(|e| e.to_string())?;
144+
db::delete_session_fact(&conn, fact_id).map_err(|e| e.to_string())?;
145+
Ok(())
146+
}
147+
148+
#[tauri::command]
149+
pub fn update_note(
150+
state: State<'_, AppState>,
151+
note_id: i64,
152+
content: String,
153+
) -> Result<(), String> {
154+
let path = state.get_db_path()?;
155+
let conn = db::open_readwrite(&path).map_err(|e| e.to_string())?;
156+
db::update_note(&conn, note_id, &content).map_err(|e| e.to_string())?;
157+
Ok(())
158+
}
159+
160+
#[tauri::command]
161+
pub fn delete_note(
162+
state: State<'_, AppState>,
163+
note_id: i64,
164+
) -> Result<(), String> {
165+
let path = state.get_db_path()?;
166+
let conn = db::open_readwrite(&path).map_err(|e| e.to_string())?;
167+
db::delete_note(&conn, note_id).map_err(|e| e.to_string())?;
168+
Ok(())
169+
}
170+
171+
#[tauri::command]
172+
pub fn dismiss_note(
173+
state: State<'_, AppState>,
174+
note_id: i64,
175+
) -> Result<(), String> {
176+
let path = state.get_db_path()?;
177+
let conn = db::open_readwrite(&path).map_err(|e| e.to_string())?;
178+
db::dismiss_note(&conn, note_id).map_err(|e| e.to_string())?;
179+
Ok(())
180+
}
181+
125182
#[tauri::command]
126183
pub fn get_session_meta(
127184
state: State<'_, AppState>,

packages/dashboard/src-tauri/src/db.rs

Lines changed: 99 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,15 @@ pub struct Compartment {
142142
pub sequence: i64,
143143
pub start_message: i64,
144144
pub end_message: i64,
145+
pub start_message_id: Option<String>,
146+
pub end_message_id: Option<String>,
145147
pub title: String,
146148
pub content: String,
147149
pub created_at: i64,
150+
/// Resolved from OpenCode DB using start_message_id
151+
pub start_time: Option<i64>,
152+
/// Resolved from OpenCode DB using end_message_id
153+
pub end_time: Option<i64>,
148154
}
149155

150156
#[derive(Debug, Serialize, Clone)]
@@ -1392,22 +1398,55 @@ pub fn get_compartments(
13921398
session_id: &str,
13931399
) -> Result<Vec<Compartment>, rusqlite::Error> {
13941400
let mut stmt = conn.prepare(
1395-
"SELECT id, session_id, sequence, start_message, end_message, title, content, created_at
1401+
"SELECT id, session_id, sequence, start_message, end_message, start_message_id, end_message_id, title, content, created_at
13961402
FROM compartments WHERE session_id = ?1 ORDER BY sequence DESC",
13971403
)?;
1398-
let rows = stmt.query_map(rusqlite::params![session_id], |row| {
1399-
Ok(Compartment {
1400-
id: row.get(0)?,
1401-
session_id: row.get(1)?,
1402-
sequence: row.get(2)?,
1403-
start_message: row.get(3)?,
1404-
end_message: row.get(4)?,
1405-
title: row.get(5)?,
1406-
content: row.get(6)?,
1407-
created_at: row.get(7)?,
1408-
})
1409-
})?;
1410-
rows.collect()
1404+
let mut compartments: Vec<Compartment> = stmt
1405+
.query_map(rusqlite::params![session_id], |row| {
1406+
Ok(Compartment {
1407+
id: row.get(0)?,
1408+
session_id: row.get(1)?,
1409+
sequence: row.get(2)?,
1410+
start_message: row.get(3)?,
1411+
end_message: row.get(4)?,
1412+
start_message_id: row.get(5)?,
1413+
end_message_id: row.get(6)?,
1414+
title: row.get(7)?,
1415+
content: row.get(8)?,
1416+
created_at: row.get(9)?,
1417+
start_time: None,
1418+
end_time: None,
1419+
})
1420+
})?
1421+
.collect::<Result<Vec<_>, _>>()?;
1422+
1423+
// Resolve message timestamps from OpenCode DB
1424+
if let Some(opencode_db_path) = resolve_opencode_db_path() {
1425+
if let Ok(oc_conn) = open_readonly(&opencode_db_path) {
1426+
for comp in compartments.iter_mut() {
1427+
if let Some(ref start_id) = comp.start_message_id {
1428+
if let Ok(ts) = oc_conn.query_row(
1429+
"SELECT time_created FROM message WHERE id = ?1",
1430+
rusqlite::params![start_id],
1431+
|row| row.get::<_, Option<i64>>(0),
1432+
) {
1433+
comp.start_time = ts;
1434+
}
1435+
}
1436+
if let Some(ref end_id) = comp.end_message_id {
1437+
if let Ok(ts) = oc_conn.query_row(
1438+
"SELECT time_created FROM message WHERE id = ?1",
1439+
rusqlite::params![end_id],
1440+
|row| row.get::<_, Option<i64>>(0),
1441+
) {
1442+
comp.end_time = ts;
1443+
}
1444+
}
1445+
}
1446+
}
1447+
}
1448+
1449+
Ok(compartments)
14111450
}
14121451

14131452
pub fn get_session_facts(
@@ -1460,6 +1499,52 @@ pub fn get_session_notes(
14601499
rows.collect()
14611500
}
14621501

1502+
pub fn update_session_fact(
1503+
conn: &Connection,
1504+
fact_id: i64,
1505+
content: &str,
1506+
) -> Result<usize, rusqlite::Error> {
1507+
conn.execute(
1508+
"UPDATE session_facts SET content = ?1, updated_at = ?2 WHERE id = ?3",
1509+
rusqlite::params![content, chrono::Utc::now().timestamp_millis(), fact_id],
1510+
)
1511+
}
1512+
1513+
pub fn delete_session_fact(
1514+
conn: &Connection,
1515+
fact_id: i64,
1516+
) -> Result<usize, rusqlite::Error> {
1517+
conn.execute("DELETE FROM session_facts WHERE id = ?1", rusqlite::params![fact_id])
1518+
}
1519+
1520+
pub fn update_note(
1521+
conn: &Connection,
1522+
note_id: i64,
1523+
content: &str,
1524+
) -> Result<usize, rusqlite::Error> {
1525+
conn.execute(
1526+
"UPDATE notes SET content = ?1, updated_at = ?2 WHERE id = ?3",
1527+
rusqlite::params![content, chrono::Utc::now().timestamp_millis(), note_id],
1528+
)
1529+
}
1530+
1531+
pub fn delete_note(
1532+
conn: &Connection,
1533+
note_id: i64,
1534+
) -> Result<usize, rusqlite::Error> {
1535+
conn.execute("DELETE FROM notes WHERE id = ?1", rusqlite::params![note_id])
1536+
}
1537+
1538+
pub fn dismiss_note(
1539+
conn: &Connection,
1540+
note_id: i64,
1541+
) -> Result<usize, rusqlite::Error> {
1542+
conn.execute(
1543+
"UPDATE notes SET status = 'dismissed', updated_at = ?1 WHERE id = ?2",
1544+
rusqlite::params![chrono::Utc::now().timestamp_millis(), note_id],
1545+
)
1546+
}
1547+
14631548
pub fn get_smart_notes(
14641549
conn: &Connection,
14651550
project_path: &str,

packages/dashboard/src-tauri/src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ fn main() {
4040
commands::get_session_facts,
4141
commands::get_session_notes,
4242
commands::get_smart_notes,
43+
commands::update_session_fact,
44+
commands::delete_session_fact,
45+
commands::update_note,
46+
commands::delete_note,
47+
commands::dismiss_note,
4348
commands::get_session_meta,
4449
commands::get_context_token_breakdown,
4550
// Dreamer

packages/dashboard/src/components/ConfigEditor/ConfigEditor.tsx

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -890,69 +890,75 @@ function ConfigForm(props: {
890890
{/* Left column: Compaction Markers + User Memories */}
891891
<div class="config-card-content">
892892
<div class="config-field">
893-
<label class="field-label">
894-
<span>Compaction Markers</span>
895-
<span class="field-hint">Inject boundary into OpenCode's DB so transform only processes the live tail</span>
896-
</label>
897-
<label class="toggle">
893+
<div class="config-field-header">
894+
<label class="config-field-label">Compaction Markers</label>
895+
<span class="config-field-key">experimental.compaction_markers</span>
896+
</div>
897+
<span class="config-field-desc">Inject boundary into OpenCode's DB so transform only processes the live tail</span>
898+
<label class="toggle-switch">
898899
<input type="checkbox" checked={compactionMarkers()} onChange={(e) => updateExp("compaction_markers", e.currentTarget.checked)} />
899900
<span class="toggle-slider" />
900901
<span class="toggle-label">{compactionMarkers() ? "Enabled" : "Disabled"}</span>
901902
</label>
902903
</div>
903904

904-
<div class="config-field" style={{ "margin-top": "16px" }}>
905-
<label class="field-label">
906-
<span>User Memories</span>
907-
<span class="field-hint">Extract behavioral observations from historian runs, promote recurring patterns to stable user memories. Requires dreamer.</span>
908-
</label>
909-
<label class="toggle">
905+
<div class="config-field">
906+
<div class="config-field-header">
907+
<label class="config-field-label">User Memories</label>
908+
<span class="config-field-key">experimental.user_memories.enabled</span>
909+
</div>
910+
<span class="config-field-desc">Extract behavioral observations from historian runs, promote recurring patterns to stable user memories. Requires dreamer.</span>
911+
<label class="toggle-switch">
910912
<input type="checkbox" checked={userMemEnabled()} onChange={(e) => updateExp("user_memories.enabled", e.currentTarget.checked)} />
911913
<span class="toggle-slider" />
912914
<span class="toggle-label">{userMemEnabled() ? "Enabled" : "Disabled"}</span>
913915
</label>
914916
</div>
915917

916918
<Show when={userMemEnabled()}>
917-
<div class="config-field" style={{ "margin-top": "8px", "padding-left": "12px" }}>
918-
<label class="field-label">
919-
<span>Promotion Threshold</span>
920-
<span class="field-hint">Minimum candidate observations before dreamer promotes to stable (2–20)</span>
921-
</label>
922-
<input type="number" class="field-input" min={2} max={20} value={userMemThreshold()} onInput={(e) => updateExp("user_memories.promotion_threshold", Number(e.currentTarget.value))} style={{ width: "80px" }} />
919+
<div class="config-field">
920+
<div class="config-field-header">
921+
<label class="config-field-label">Promotion Threshold</label>
922+
<span class="config-field-key">experimental.user_memories.promotion_threshold</span>
923+
</div>
924+
<span class="config-field-desc">Minimum candidate observations before dreamer promotes to stable (2–20)</span>
925+
<input class="config-input" type="number" min={2} max={20} value={userMemThreshold()} onInput={(e) => updateExp("user_memories.promotion_threshold", Number(e.currentTarget.value))} />
923926
</div>
924927
</Show>
925928
</div>
926929

927930
{/* Right column: Key File Pinning */}
928931
<div class="config-card-content">
929932
<div class="config-field">
930-
<label class="field-label">
931-
<span>Key File Pinning</span>
932-
<span class="field-hint">Pin frequently-read files into the system prompt so the agent doesn't need to re-read them after drops. Requires dreamer.</span>
933-
</label>
934-
<label class="toggle">
933+
<div class="config-field-header">
934+
<label class="config-field-label">Key File Pinning</label>
935+
<span class="config-field-key">experimental.pin_key_files.enabled</span>
936+
</div>
937+
<span class="config-field-desc">Pin frequently-read files into the system prompt so the agent doesn't need to re-read them after drops. Requires dreamer.</span>
938+
<label class="toggle-switch">
935939
<input type="checkbox" checked={pinEnabled()} onChange={(e) => updateExp("pin_key_files.enabled", e.currentTarget.checked)} />
936940
<span class="toggle-slider" />
937941
<span class="toggle-label">{pinEnabled() ? "Enabled" : "Disabled"}</span>
938942
</label>
939943
</div>
940944

941945
<Show when={pinEnabled()}>
942-
<div class="config-field" style={{ "margin-top": "8px", "padding-left": "12px" }}>
943-
<label class="field-label">
944-
<span>Token Budget</span>
945-
<span class="field-hint">Total tokens for all pinned key files (2,000–30,000)</span>
946-
</label>
947-
<input type="number" class="field-input" min={2000} max={30000} step={1000} value={pinBudget()} onInput={(e) => updateExp("pin_key_files.token_budget", Number(e.currentTarget.value))} style={{ width: "100px" }} />
946+
<div class="config-field">
947+
<div class="config-field-header">
948+
<label class="config-field-label">Token Budget</label>
949+
<span class="config-field-key">experimental.pin_key_files.token_budget</span>
950+
</div>
951+
<span class="config-field-desc">Total tokens for all pinned key files (2,000–30,000)</span>
952+
<input class="config-input" type="number" min={2000} max={30000} step={1000} value={pinBudget()} onInput={(e) => updateExp("pin_key_files.token_budget", Number(e.currentTarget.value))} />
948953
</div>
949954

950-
<div class="config-field" style={{ "margin-top": "8px", "padding-left": "12px" }}>
951-
<label class="field-label">
952-
<span>Min Reads</span>
953-
<span class="field-hint">Minimum full-read count before a file is eligible for pinning (2–20)</span>
954-
</label>
955-
<input type="number" class="field-input" min={2} max={20} value={pinMinReads()} onInput={(e) => updateExp("pin_key_files.min_reads", Number(e.currentTarget.value))} style={{ width: "80px" }} />
955+
<div class="config-field">
956+
<div class="config-field-header">
957+
<label class="config-field-label">Min Reads</label>
958+
<span class="config-field-key">experimental.pin_key_files.min_reads</span>
959+
</div>
960+
<span class="config-field-desc">Minimum full-read count before a file is eligible for pinning (2–20)</span>
961+
<input class="config-input" type="number" min={2} max={20} value={pinMinReads()} onInput={(e) => updateExp("pin_key_files.min_reads", Number(e.currentTarget.value))} />
956962
</div>
957963
</Show>
958964
</div>

0 commit comments

Comments
 (0)