fix(btmsg): convert positional column access to named, fix camelCase mismatch

CRITICAL: get_agents() used SELECT * positional index 7 for status,
but column 7 is system_prompt (column 8 is status). Converted all
query functions in btmsg.rs and bttask.rs to named column access.

Fixed BtmsgAgent/BtmsgMessage TypeScript interfaces to use camelCase
matching Rust serde(rename_all = camelCase). Updated CommsTab consumer.
This commit is contained in:
Hibryda 2026-03-11 21:54:19 +01:00
parent 32f6d7eadf
commit 93c2cdf434
4 changed files with 99 additions and 88 deletions

View file

@ -1,4 +1,4 @@
// btmsg — Read-only access to btmsg SQLite database
// btmsg — Access to btmsg SQLite database
// Database at ~/.local/share/bterminal/btmsg.db (created by btmsg CLI)
use rusqlite::{params, Connection, OpenFlags};
@ -17,8 +17,13 @@ fn open_db() -> Result<Connection, String> {
if !path.exists() {
return Err("btmsg database not found. Run 'btmsg register' first.".into());
}
Connection::open_with_flags(&path, OpenFlags::SQLITE_OPEN_READ_WRITE)
.map_err(|e| format!("Failed to open btmsg.db: {e}"))
let conn = Connection::open_with_flags(&path, OpenFlags::SQLITE_OPEN_READ_WRITE)
.map_err(|e| format!("Failed to open btmsg.db: {e}"))?;
conn.pragma_update(None, "journal_mode", "WAL")
.map_err(|e| format!("Failed to set WAL mode: {e}"))?;
conn.pragma_update(None, "busy_timeout", 5000)
.map_err(|e| format!("Failed to set busy_timeout: {e}"))?;
Ok(conn)
}
#[derive(Debug, Serialize, Deserialize)]
@ -95,13 +100,13 @@ pub fn get_agents(group_id: &str) -> Result<Vec<BtmsgAgent>, String> {
let agents = stmt.query_map(params![group_id], |row| {
Ok(BtmsgAgent {
id: row.get(0)?,
name: row.get(1)?,
role: row.get(2)?,
group_id: row.get(3)?,
tier: row.get(4)?,
model: row.get(5)?,
status: row.get::<_, Option<String>>(7)?.unwrap_or_else(|| "stopped".into()),
id: row.get("id")?,
name: row.get("name")?,
role: row.get("role")?,
group_id: row.get("group_id")?,
tier: row.get("tier")?,
model: row.get("model")?,
status: row.get::<_, Option<String>>("status")?.unwrap_or_else(|| "stopped".into()),
unread_count: row.get("unread_count")?,
})
}).map_err(|e| format!("Query error: {e}"))?;
@ -122,22 +127,22 @@ pub fn unread_messages(agent_id: &str) -> Result<Vec<BtmsgMessage>, String> {
let db = open_db()?;
let mut stmt = db.prepare(
"SELECT m.id, m.from_agent, m.to_agent, m.content, m.read, m.reply_to, m.created_at, \
a.name, a.role \
a.name AS sender_name, a.role AS sender_role \
FROM messages m JOIN agents a ON m.from_agent = a.id \
WHERE m.to_agent = ? AND m.read = 0 ORDER BY m.created_at ASC"
).map_err(|e| format!("Query error: {e}"))?;
let msgs = stmt.query_map(params![agent_id], |row| {
Ok(BtmsgMessage {
id: row.get(0)?,
from_agent: row.get(1)?,
to_agent: row.get(2)?,
content: row.get(3)?,
read: row.get::<_, i32>(4)? != 0,
reply_to: row.get(5)?,
created_at: row.get(6)?,
sender_name: row.get(7)?,
sender_role: row.get(8)?,
id: row.get("id")?,
from_agent: row.get("from_agent")?,
to_agent: row.get("to_agent")?,
content: row.get("content")?,
read: row.get::<_, i32>("read")? != 0,
reply_to: row.get("reply_to")?,
created_at: row.get("created_at")?,
sender_name: row.get("sender_name")?,
sender_role: row.get("sender_role")?,
})
}).map_err(|e| format!("Query error: {e}"))?;
@ -148,7 +153,7 @@ pub fn history(agent_id: &str, other_id: &str, limit: i32) -> Result<Vec<BtmsgMe
let db = open_db()?;
let mut stmt = db.prepare(
"SELECT m.id, m.from_agent, m.to_agent, m.content, m.read, m.reply_to, m.created_at, \
a.name, a.role \
a.name AS sender_name, a.role AS sender_role \
FROM messages m JOIN agents a ON m.from_agent = a.id \
WHERE (m.from_agent = ?1 AND m.to_agent = ?2) OR (m.from_agent = ?2 AND m.to_agent = ?1) \
ORDER BY m.created_at ASC LIMIT ?3"
@ -156,15 +161,15 @@ pub fn history(agent_id: &str, other_id: &str, limit: i32) -> Result<Vec<BtmsgMe
let msgs = stmt.query_map(params![agent_id, other_id, limit], |row| {
Ok(BtmsgMessage {
id: row.get(0)?,
from_agent: row.get(1)?,
to_agent: row.get(2)?,
content: row.get(3)?,
read: row.get::<_, i32>(4)? != 0,
reply_to: row.get(5)?,
created_at: row.get(6)?,
sender_name: row.get(7)?,
sender_role: row.get(8)?,
id: row.get("id")?,
from_agent: row.get("from_agent")?,
to_agent: row.get("to_agent")?,
content: row.get("content")?,
read: row.get::<_, i32>("read")? != 0,
reply_to: row.get("reply_to")?,
created_at: row.get("created_at")?,
sender_name: row.get("sender_name")?,
sender_role: row.get("sender_role")?,
})
}).map_err(|e| format!("Query error: {e}"))?;
@ -257,7 +262,8 @@ pub fn all_feed(group_id: &str, limit: i32) -> Result<Vec<BtmsgFeedMessage>, Str
let db = open_db()?;
let mut stmt = db.prepare(
"SELECT m.id, m.from_agent, m.to_agent, m.content, m.created_at, m.reply_to, \
a1.name, a1.role, a2.name, a2.role \
a1.name AS sender_name, a1.role AS sender_role, \
a2.name AS recipient_name, a2.role AS recipient_role \
FROM messages m \
JOIN agents a1 ON m.from_agent = a1.id \
JOIN agents a2 ON m.to_agent = a2.id \
@ -267,16 +273,16 @@ pub fn all_feed(group_id: &str, limit: i32) -> Result<Vec<BtmsgFeedMessage>, Str
let msgs = stmt.query_map(params![group_id, limit], |row| {
Ok(BtmsgFeedMessage {
id: row.get(0)?,
from_agent: row.get(1)?,
to_agent: row.get(2)?,
content: row.get(3)?,
created_at: row.get(4)?,
reply_to: row.get(5)?,
sender_name: row.get(6)?,
sender_role: row.get(7)?,
recipient_name: row.get(8)?,
recipient_role: row.get(9)?,
id: row.get("id")?,
from_agent: row.get("from_agent")?,
to_agent: row.get("to_agent")?,
content: row.get("content")?,
created_at: row.get("created_at")?,
reply_to: row.get("reply_to")?,
sender_name: row.get("sender_name")?,
sender_role: row.get("sender_role")?,
recipient_name: row.get("recipient_name")?,
recipient_role: row.get("recipient_role")?,
})
}).map_err(|e| format!("Query error: {e}"))?;
@ -296,19 +302,19 @@ pub fn get_channels(group_id: &str) -> Result<Vec<BtmsgChannel>, String> {
let db = open_db()?;
let mut stmt = db.prepare(
"SELECT c.id, c.name, c.group_id, c.created_by, \
(SELECT COUNT(*) FROM channel_members cm WHERE cm.channel_id = c.id), \
(SELECT COUNT(*) FROM channel_members cm WHERE cm.channel_id = c.id) AS member_count, \
c.created_at \
FROM channels c WHERE c.group_id = ? ORDER BY c.name"
).map_err(|e| format!("Query error: {e}"))?;
let channels = stmt.query_map(params![group_id], |row| {
Ok(BtmsgChannel {
id: row.get(0)?,
name: row.get(1)?,
group_id: row.get(2)?,
created_by: row.get(3)?,
member_count: row.get(4)?,
created_at: row.get(5)?,
id: row.get("id")?,
name: row.get("name")?,
group_id: row.get("group_id")?,
created_by: row.get("created_by")?,
member_count: row.get("member_count")?,
created_at: row.get("created_at")?,
})
}).map_err(|e| format!("Query error: {e}"))?;
@ -319,20 +325,20 @@ pub fn get_channel_messages(channel_id: &str, limit: i32) -> Result<Vec<BtmsgCha
let db = open_db()?;
let mut stmt = db.prepare(
"SELECT cm.id, cm.channel_id, cm.from_agent, cm.content, cm.created_at, \
a.name, a.role \
a.name AS sender_name, a.role AS sender_role \
FROM channel_messages cm JOIN agents a ON cm.from_agent = a.id \
WHERE cm.channel_id = ? ORDER BY cm.created_at ASC LIMIT ?"
).map_err(|e| format!("Query error: {e}"))?;
let msgs = stmt.query_map(params![channel_id, limit], |row| {
Ok(BtmsgChannelMessage {
id: row.get(0)?,
channel_id: row.get(1)?,
from_agent: row.get(2)?,
content: row.get(3)?,
created_at: row.get(4)?,
sender_name: row.get(5)?,
sender_role: row.get(6)?,
id: row.get("id")?,
channel_id: row.get("channel_id")?,
from_agent: row.get("from_agent")?,
content: row.get("content")?,
created_at: row.get("created_at")?,
sender_name: row.get("sender_name")?,
sender_role: row.get("sender_role")?,
})
}).map_err(|e| format!("Query error: {e}"))?;

View file

@ -17,8 +17,13 @@ fn open_db() -> Result<Connection, String> {
if !path.exists() {
return Err("btmsg database not found".into());
}
Connection::open_with_flags(&path, OpenFlags::SQLITE_OPEN_READ_WRITE)
.map_err(|e| format!("Failed to open btmsg.db: {e}"))
let conn = Connection::open_with_flags(&path, OpenFlags::SQLITE_OPEN_READ_WRITE)
.map_err(|e| format!("Failed to open btmsg.db: {e}"))?;
conn.pragma_update(None, "journal_mode", "WAL")
.map_err(|e| format!("Failed to set WAL mode: {e}"))?;
conn.pragma_update(None, "busy_timeout", 5000)
.map_err(|e| format!("Failed to set busy_timeout: {e}"))?;
Ok(conn)
}
#[derive(Debug, Serialize, Deserialize)]
@ -64,18 +69,18 @@ pub fn list_tasks(group_id: &str) -> Result<Vec<Task>, String> {
let rows = stmt
.query_map(params![group_id], |row| {
Ok(Task {
id: row.get(0)?,
title: row.get(1)?,
description: row.get::<_, String>(2).unwrap_or_default(),
status: row.get::<_, String>(3).unwrap_or_else(|_| "todo".into()),
priority: row.get::<_, String>(4).unwrap_or_else(|_| "medium".into()),
assigned_to: row.get(5)?,
created_by: row.get(6)?,
group_id: row.get(7)?,
parent_task_id: row.get(8)?,
sort_order: row.get::<_, i32>(9).unwrap_or(0),
created_at: row.get::<_, String>(10).unwrap_or_default(),
updated_at: row.get::<_, String>(11).unwrap_or_default(),
id: row.get("id")?,
title: row.get("title")?,
description: row.get::<_, String>("description").unwrap_or_default(),
status: row.get::<_, String>("status").unwrap_or_else(|_| "todo".into()),
priority: row.get::<_, String>("priority").unwrap_or_else(|_| "medium".into()),
assigned_to: row.get("assigned_to")?,
created_by: row.get("created_by")?,
group_id: row.get("group_id")?,
parent_task_id: row.get("parent_task_id")?,
sort_order: row.get::<_, i32>("sort_order").unwrap_or(0),
created_at: row.get::<_, String>("created_at").unwrap_or_default(),
updated_at: row.get::<_, String>("updated_at").unwrap_or_default(),
})
})
.map_err(|e| format!("Query error: {e}"))?;
@ -98,11 +103,11 @@ pub fn task_comments(task_id: &str) -> Result<Vec<TaskComment>, String> {
let rows = stmt
.query_map(params![task_id], |row| {
Ok(TaskComment {
id: row.get(0)?,
task_id: row.get(1)?,
agent_id: row.get(2)?,
content: row.get(3)?,
created_at: row.get::<_, String>(4).unwrap_or_default(),
id: row.get("id")?,
task_id: row.get("task_id")?,
agent_id: row.get("agent_id")?,
content: row.get("content")?,
created_at: row.get::<_, String>("created_at").unwrap_or_default(),
})
})
.map_err(|e| format!("Query error: {e}"))?;

View file

@ -10,23 +10,23 @@ export interface BtmsgAgent {
id: string;
name: string;
role: string;
group_id: string;
groupId: string;
tier: number;
model: string | null;
status: string;
unread_count: number;
unreadCount: number;
}
export interface BtmsgMessage {
id: string;
from_agent: string;
to_agent: string;
fromAgent: string;
toAgent: string;
content: string;
read: boolean;
reply_to: string | null;
created_at: string;
sender_name?: string;
sender_role?: string;
replyTo: string | null;
createdAt: string;
senderName?: string;
senderRole?: string;
}
export interface BtmsgFeedMessage {

View file

@ -257,8 +257,8 @@
<span class="conv-icon">{getAgentIcon(agent.role)}</span>
<span class="conv-name">{agent.name}</span>
<span class="status-dot {statusClass}"></span>
{#if agent.unread_count > 0}
<span class="unread-badge">{agent.unread_count}</span>
{#if agent.unreadCount > 0}
<span class="unread-badge">{agent.unreadCount}</span>
{/if}
</button>
{/each}
@ -301,11 +301,11 @@
<div class="empty-state">No messages yet. Start the conversation!</div>
{:else}
{#each dmMessages as msg (msg.id)}
{@const isMe = msg.from_agent === ADMIN_ID}
{@const isMe = msg.fromAgent === ADMIN_ID}
<div class="message" class:own={isMe}>
<div class="msg-header">
<span class="msg-sender">{isMe ? 'You' : (msg.sender_name ?? msg.from_agent)}</span>
<span class="msg-time">{formatTime(msg.created_at)}</span>
<span class="msg-sender">{isMe ? 'You' : (msg.senderName ?? msg.fromAgent)}</span>
<span class="msg-time">{formatTime(msg.createdAt)}</span>
</div>
<div class="msg-content">{msg.content}</div>
</div>