Claude Code engine, image viewer, colour tags, checks per turn and more
- Claude Code as an engine: chats on a "claude-code/<alias>" model run through the claude CLI (stream-json); tool approvals go through our permission system. Sign in from the web UI (URL + pasted code), CLI version and update from Settings; the Docker image contains the CLI. - Small models (8k context) for side tasks: input is cut to the model's context window; one-line chat status, "What does this do?" on approvals, "Plan as automation" (job draft from a chat). - Mobile: top bar no longer widens the page, image viewer (zoom, swipe, mark up, use in message), jump to previous own message, message menu, time under every message. - Approvals: push actions (allow/reject), "always allow in this project" (never for dangerous commands) with revoke. - Per turn: changed files with diffs and rollback; project check command with a green/red badge and "let the agent fix it". - Colour tags for chats in the colours of the active theme. - Project defaults for new chats, project page (timeline), offline queue. Tested in a throwaway container with a mock model (8k small model): 26 API checks passed (permissions, diffs, checks, defaults, job draft, side-task budget, Claude Code status/login URL, unsigned-in chat). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
084452abf6
commit
62fe5bf467
42 files changed
+3734
-55
No files matched your search
+141
-3
@@ -157,6 +157,31 @@ impl Store {
|
||||
content BLOB
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS checkpoints_session ON checkpoints(session_id, ts);
|
||||
CREATE TABLE IF NOT EXISTS project_approvals (
|
||||
cwd TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (cwd, key)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS project_defaults (
|
||||
cwd TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS project_checks (
|
||||
cwd TEXT PRIMARY KEY,
|
||||
command TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS turn_checks (
|
||||
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
||||
message_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
exit_code INTEGER,
|
||||
duration_ms INTEGER NOT NULL DEFAULT 0,
|
||||
output TEXT NOT NULL DEFAULT '',
|
||||
ts INTEGER NOT NULL,
|
||||
PRIMARY KEY (session_id, message_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS session_shares (
|
||||
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
@@ -1481,6 +1506,114 @@ impl Store {
|
||||
}
|
||||
}
|
||||
|
||||
// --- project overview ------------------------------------------------------------------------
|
||||
|
||||
impl Store {
|
||||
/// Per chat: files the agent changed, the latest check result and the cost.
|
||||
pub fn project_overview(&self, session_ids: &[String]) -> Result<Value> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut out = serde_json::Map::new();
|
||||
for id in session_ids {
|
||||
let files: i64 = conn.query_row("SELECT COUNT(DISTINCT path) FROM checkpoints WHERE session_id = ?1", params![id], |r| r.get(0))?;
|
||||
let check: Option<String> = conn
|
||||
.query_row("SELECT status FROM turn_checks WHERE session_id = ?1 ORDER BY ts DESC LIMIT 1", params![id], |r| r.get(0))
|
||||
.optional()?;
|
||||
let failed: i64 = conn.query_row("SELECT COUNT(*) FROM turn_checks WHERE session_id = ?1 AND status IN ('failed','timeout')", params![id], |r| r.get(0))?;
|
||||
let cost: Option<f64> = conn.query_row("SELECT SUM(cost) FROM usage WHERE root_session_id = ?1", params![id], |r| r.get(0))?;
|
||||
out.insert(id.clone(), json!({"files": files, "check": check, "checks_failed": failed, "cost": cost.unwrap_or(0.0)}));
|
||||
}
|
||||
Ok(Value::Object(out))
|
||||
}
|
||||
}
|
||||
|
||||
// --- defaults of new chats in a project -------------------------------------------------------
|
||||
|
||||
impl Store {
|
||||
/// {model, agent, permission_mode, color, instructions, folder} new chats of the folder start with.
|
||||
pub fn project_defaults(&self, cwd: &str) -> Result<Option<Value>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let v: Option<String> = conn.query_row("SELECT data FROM project_defaults WHERE cwd = ?1", params![cwd], |r| r.get(0)).optional()?;
|
||||
Ok(v.and_then(|v| serde_json::from_str(&v).ok()))
|
||||
}
|
||||
|
||||
pub fn set_project_defaults(&self, cwd: &str, data: Option<&Value>) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
match data {
|
||||
Some(d) => conn.execute("INSERT INTO project_defaults (cwd, data) VALUES (?1, ?2) ON CONFLICT(cwd) DO UPDATE SET data = ?2", params![cwd, d.to_string()])?,
|
||||
None => conn.execute("DELETE FROM project_defaults WHERE cwd = ?1", params![cwd])?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// --- checks after a turn ------------------------------------------------------------------
|
||||
|
||||
impl Store {
|
||||
/// (command, enabled) of a project folder.
|
||||
pub fn project_check(&self, cwd: &str) -> Result<Option<(String, bool)>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
Ok(conn.query_row("SELECT command, enabled FROM project_checks WHERE cwd = ?1", params![cwd], |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)? != 0))).optional()?)
|
||||
}
|
||||
|
||||
pub fn set_project_check(&self, cwd: &str, command: &str, enabled: bool) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
if command.trim().is_empty() {
|
||||
conn.execute("DELETE FROM project_checks WHERE cwd = ?1", params![cwd])?;
|
||||
} else {
|
||||
conn.execute(
|
||||
"INSERT INTO project_checks (cwd, command, enabled) VALUES (?1, ?2, ?3) ON CONFLICT(cwd) DO UPDATE SET command = ?2, enabled = ?3",
|
||||
params![cwd, command.trim(), enabled as i64],
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_turn_check(&self, session_id: &str, message_id: &str, status: &str, exit_code: Option<i32>, duration_ms: u64, output: &str) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO turn_checks (session_id, message_id, status, exit_code, duration_ms, output, ts) VALUES (?1,?2,?3,?4,?5,?6,?7)
|
||||
ON CONFLICT(session_id, message_id) DO UPDATE SET status = ?3, exit_code = ?4, duration_ms = ?5, output = ?6, ts = ?7",
|
||||
params![session_id, message_id, status, exit_code, duration_ms as i64, output, now_ms()],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn turn_checks(&self, session_id: &str) -> Result<Vec<Value>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare("SELECT message_id, status, exit_code, duration_ms, output FROM turn_checks WHERE session_id = ?1 ORDER BY ts DESC LIMIT 300")?;
|
||||
let rows = stmt
|
||||
.query_map(params![session_id], |r| {
|
||||
Ok(json!({"message_id": r.get::<_, String>(0)?, "status": r.get::<_, String>(1)?, "exit_code": r.get::<_, Option<i32>>(2)?, "duration_ms": r.get::<_, i64>(3)?, "output": r.get::<_, String>(4)?}))
|
||||
})?
|
||||
.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||
Ok(rows)
|
||||
}
|
||||
}
|
||||
|
||||
// --- project-wide approvals ---------------------------------------------------------
|
||||
|
||||
impl Store {
|
||||
/// Approvals ("bash:<prefix>" or a tool name) remembered for a project folder.
|
||||
pub fn project_approvals(&self, cwd: &str) -> Result<Vec<String>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare("SELECT key FROM project_approvals WHERE cwd = ?1 ORDER BY key")?;
|
||||
let rows = stmt.query_map(params![cwd], |r| r.get::<_, String>(0))?.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub fn add_project_approval(&self, cwd: &str, key: &str) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute("INSERT OR IGNORE INTO project_approvals (cwd, key, created_at) VALUES (?1, ?2, ?3)", params![cwd, key, now_ms()])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_project_approval(&self, cwd: &str, key: &str) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute("DELETE FROM project_approvals WHERE cwd = ?1 AND key = ?2", params![cwd, key])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// --- file checkpoints ------------------------------------------------------------
|
||||
|
||||
impl Store {
|
||||
@@ -1496,13 +1629,18 @@ impl Store {
|
||||
|
||||
/// Per path, the oldest snapshot taken at or after `since` (ms): the state to restore.
|
||||
pub fn checkpoints_since(&self, session_id: &str, since: i64) -> Result<Vec<(String, Option<Vec<u8>>)>> {
|
||||
self.checkpoints_between(session_id, since, i64::MAX)
|
||||
}
|
||||
|
||||
/// Per file the state before its first change in the time range [since, until).
|
||||
pub fn checkpoints_between(&self, session_id: &str, since: i64, until: i64) -> Result<Vec<(String, Option<Vec<u8>>)>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT path, existed, content FROM checkpoints c WHERE session_id = ?1 AND ts >= ?2
|
||||
AND id = (SELECT MIN(id) FROM checkpoints WHERE session_id = ?1 AND ts >= ?2 AND path = c.path)",
|
||||
"SELECT path, existed, content FROM checkpoints c WHERE session_id = ?1 AND ts >= ?2 AND ts < ?3
|
||||
AND id = (SELECT MIN(id) FROM checkpoints WHERE session_id = ?1 AND ts >= ?2 AND ts < ?3 AND path = c.path)",
|
||||
)?;
|
||||
let rows = stmt
|
||||
.query_map(params![session_id, since], |r| {
|
||||
.query_map(params![session_id, since, until], |r| {
|
||||
let existed: i64 = r.get(1)?;
|
||||
Ok((r.get::<_, String>(0)?, if existed != 0 { r.get::<_, Option<Vec<u8>>>(2)? } else { None }))
|
||||
})?
|
||||
|
||||
Reference in new issue
Block a user