Agent + conversation pickers, prefs persistence
- tray submenus: Agent (from letta agents list, radio-style) and Conversation (from local backend storage, CLI has no listing surface) - conversation entries show preview: summary or last-message text - conversation menu refreshes on aboutToShow (KDE DBus menu syncs a step behind eager mutation) - CLI rule honored: --agent only with --conversation default; named conversations imply the agent - plain-text fallback parsing for non-stream-json output - empty-reply debug: raw stream tail dump - prefs: ~/.local/inferon/prefs.json, saved on pick, loaded at startup, HOME-resolved paths (std.c.getenv), owned buffers (no alias double-free) - leak fixes: agent name fallback, quit-path drains for menu entries 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code <noreply@letta.com>
This commit is contained in:
1 parent
bcd48a3443
commit
2fdc812a13
2 files changed
+416
-9
No files matched your search
+208
-1
@@ -70,6 +70,78 @@ var tray_icon: QSystemTrayIcon = undefined;
|
||||
var allocator: std.mem.Allocator = undefined;
|
||||
|
||||
var conversation_active: bool = false;
|
||||
var agent_menu: QMenu = undefined;
|
||||
var conv_menu: QMenu = undefined;
|
||||
var current_agent: []const u8 = LETTA_AGENT_ID;
|
||||
var current_conversation: []const u8 = "default";
|
||||
|
||||
// ---------------------------------------------------------------- prefs ---
|
||||
|
||||
var prefs_dir_buf: [512]u8 = undefined;
|
||||
var prefs_path_buf: [576]u8 = undefined;
|
||||
var PREFS_DIR: []const u8 = "";
|
||||
var PREFS_PATH: []const u8 = "";
|
||||
|
||||
/// Resolve prefs paths from $HOME once at startup.
|
||||
fn initPrefsPaths() void {
|
||||
const home = std.c.getenv("HOME") orelse return;
|
||||
const home_s = std.mem.span(home);
|
||||
PREFS_DIR = std.fmt.bufPrint(&prefs_dir_buf, "{s}/.local/inferon", .{home_s}) catch return;
|
||||
PREFS_PATH = std.fmt.bufPrint(&prefs_path_buf, "{s}/prefs.json", .{PREFS_DIR}) catch return;
|
||||
}
|
||||
|
||||
var prefs_agent_buf: ?[]u8 = null;
|
||||
var prefs_convo_buf: ?[]u8 = null;
|
||||
|
||||
fn setPrefsAgent(id: []const u8) void {
|
||||
if (allocator.dupe(u8, id)) |d| {
|
||||
if (prefs_agent_buf) |old| allocator.free(old);
|
||||
prefs_agent_buf = d;
|
||||
current_agent = d;
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
fn setPrefsConvo(id: []const u8) void {
|
||||
if (allocator.dupe(u8, id)) |d| {
|
||||
if (prefs_convo_buf) |old| allocator.free(old);
|
||||
prefs_convo_buf = d;
|
||||
current_conversation = d;
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
fn savePrefs() void {
|
||||
const io = io_ctx orelse return;
|
||||
Io.Dir.createDirAbsolute(io, PREFS_DIR, .default_dir) catch {};
|
||||
const f = Io.Dir.createFileAbsolute(io, PREFS_PATH, .{ .truncate = true }) catch return;
|
||||
var buf: [1024]u8 = undefined;
|
||||
const body = std.fmt.bufPrint(&buf,
|
||||
"{{\"agent\": \"{s}\", \"conversation\": \"{s}\"}}\n",
|
||||
.{ current_agent, current_conversation },
|
||||
) catch return;
|
||||
var wbuf: [1024]u8 = undefined;
|
||||
var w = f.writer(io, &wbuf);
|
||||
w.interface.writeAll(body) catch {};
|
||||
w.interface.flush() catch {};
|
||||
f.close(io);
|
||||
}
|
||||
|
||||
fn loadPrefs() void {
|
||||
const io = io_ctx orelse return;
|
||||
var f = Io.Dir.openFileAbsolute(io, PREFS_PATH, .{}) catch return;
|
||||
defer f.close(io);
|
||||
var buf: [1024]u8 = undefined;
|
||||
var rbuf: [1024]u8 = undefined;
|
||||
var r = f.reader(io, &rbuf);
|
||||
const n = r.interface.readSliceShort(&buf) catch return;
|
||||
if (letta.jsonStr(allocator, buf[0..n], 0, "agent")) |a| {
|
||||
setPrefsAgent(a.val);
|
||||
allocator.free(a.val);
|
||||
}
|
||||
if (letta.jsonStr(allocator, buf[0..n], 0, "conversation")) |c| {
|
||||
setPrefsConvo(c.val);
|
||||
allocator.free(c.val);
|
||||
}
|
||||
}
|
||||
var last_user_text: ?[]u8 = null;
|
||||
|
||||
fn setLastUserText(text: []u8) void {
|
||||
@@ -324,7 +396,7 @@ fn processUtterance() void {
|
||||
|
||||
pushOverlay(.begin, null);
|
||||
if (allocator.dupe(u8, text) catch null) |user_dup| pushOverlay(.user, user_dup);
|
||||
const response = letta.streamInfer(io, allocator, LETTA_AGENT_ID, text, null, onLettaEvent) catch |err| {
|
||||
const response = letta.streamInferConv(io, allocator, current_agent, current_conversation, text, null, onLettaEvent) catch |err| {
|
||||
std.log.err("letta failed: {s}", .{@errorName(err)});
|
||||
setState(.idle);
|
||||
return;
|
||||
@@ -370,6 +442,110 @@ fn onTrayActivated(_: QSystemTrayIcon, reason: i32) callconv(.c) void {
|
||||
}
|
||||
}
|
||||
|
||||
fn onToggleWindow(a: QAction) callconv(.c) void {
|
||||
if (a.isChecked()) {
|
||||
if (overlay) |*ov| ov.window.show();
|
||||
} else {
|
||||
if (overlay) |*ov| ov.hide();
|
||||
}
|
||||
}
|
||||
|
||||
const AgentEntry = struct { action: QAction, id: []u8 };
|
||||
var agent_entries: std.ArrayList(AgentEntry) = .empty;
|
||||
|
||||
fn onPickAgent(a: QAction) callconv(.c) void {
|
||||
for (agent_entries.items) |e| {
|
||||
e.action.setChecked(e.action.ptr == a.ptr);
|
||||
if (e.action.ptr == a.ptr) setPrefsAgent(e.id);
|
||||
}
|
||||
setPrefsConvo("default");
|
||||
populateConvMenu();
|
||||
savePrefs();
|
||||
std.debug.print("inferon: agent -> {s}\n", .{current_agent});
|
||||
}
|
||||
|
||||
fn onConvMenuShow(_: QMenu) callconv(.c) void {
|
||||
populateConvMenu();
|
||||
}
|
||||
|
||||
fn populateAgentMenu() void {
|
||||
for (agent_entries.items) |e| allocator.free(e.id);
|
||||
agent_entries.clearRetainingCapacity();
|
||||
agent_menu.clear();
|
||||
|
||||
const agents = letta.listAgents(io_ctx.?, allocator) catch return;
|
||||
defer {
|
||||
for (agents) |ag| {
|
||||
allocator.free(ag.id);
|
||||
allocator.free(ag.name);
|
||||
}
|
||||
allocator.free(agents);
|
||||
}
|
||||
for (agents) |ag| {
|
||||
const label = std.fmt.allocPrint(allocator, "{s}", .{ag.name}) catch continue;
|
||||
defer allocator.free(label);
|
||||
const a = QAction.new5(label, QWidget{ .ptr = null });
|
||||
a.setCheckable(true);
|
||||
a.setChecked(std.mem.eql(u8, ag.id, current_agent));
|
||||
a.onTriggered(onPickAgent);
|
||||
agent_menu.addAction(a);
|
||||
const id_dup = allocator.dupe(u8, ag.id) catch continue;
|
||||
agent_entries.append(allocator, .{ .action = a, .id = id_dup }) catch allocator.free(id_dup);
|
||||
}
|
||||
}
|
||||
|
||||
const ConvEntry = struct { action: QAction, id: []u8, preview: []u8 };
|
||||
var conv_entries: std.ArrayList(ConvEntry) = .empty;
|
||||
|
||||
fn onPickConv(a: QAction) callconv(.c) void {
|
||||
for (conv_entries.items) |e| {
|
||||
e.action.setChecked(e.action.ptr == a.ptr);
|
||||
if (e.action.ptr == a.ptr) setPrefsConvo(e.id);
|
||||
}
|
||||
savePrefs();
|
||||
std.debug.print("inferon: conversation -> {s}\n", .{current_conversation});
|
||||
}
|
||||
|
||||
fn populateConvMenu() void {
|
||||
for (conv_entries.items) |e| {
|
||||
allocator.free(e.id);
|
||||
allocator.free(e.preview);
|
||||
}
|
||||
conv_entries.clearRetainingCapacity();
|
||||
conv_menu.clear();
|
||||
|
||||
const convs = letta.listConversations(io_ctx.?, allocator, current_agent) catch return;
|
||||
defer {
|
||||
for (convs) |c| {
|
||||
allocator.free(c.id);
|
||||
allocator.free(c.preview);
|
||||
}
|
||||
allocator.free(convs);
|
||||
}
|
||||
for (convs) |c| {
|
||||
const trunc = if (c.preview.len > 60) c.preview[0..60] else c.preview;
|
||||
const label = if (trunc.len > 0)
|
||||
std.fmt.allocPrint(allocator, "{s} — {s}", .{ c.id, trunc }) catch continue
|
||||
else
|
||||
allocator.dupe(u8, c.id) catch continue;
|
||||
defer allocator.free(label);
|
||||
const a = QAction.new5(label, QWidget{ .ptr = null });
|
||||
a.setCheckable(true);
|
||||
a.setChecked(std.mem.eql(u8, c.id, current_conversation));
|
||||
a.onTriggered(onPickConv);
|
||||
conv_menu.addAction(a);
|
||||
const id_dup = allocator.dupe(u8, c.id) catch continue;
|
||||
const prev_dup = allocator.dupe(u8, c.preview) catch {
|
||||
allocator.free(id_dup);
|
||||
continue;
|
||||
};
|
||||
conv_entries.append(allocator, .{ .id = id_dup, .action = a, .preview = prev_dup }) catch {
|
||||
allocator.free(id_dup);
|
||||
allocator.free(prev_dup);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn onEndConversation(_: QAction) callconv(.c) void {
|
||||
if (recorder != null) stopRecording();
|
||||
conversation_active = false;
|
||||
@@ -444,7 +620,28 @@ pub fn main(init: std.process.Init) !void {
|
||||
const end_action = QAction.new5("&End conversation", QWidget{ .ptr = null });
|
||||
end_action.onTriggered(onEndConversation);
|
||||
|
||||
// Floating-window toggle
|
||||
const win_action = QAction.new5("&Floating window", QWidget{ .ptr = null });
|
||||
win_action.setCheckable(true);
|
||||
win_action.setChecked(true);
|
||||
win_action.onTriggered(onToggleWindow);
|
||||
|
||||
initPrefsPaths();
|
||||
letta.initConversationsDir();
|
||||
loadPrefs();
|
||||
agent_menu = QMenu.new(QWidget{ .ptr = null });
|
||||
agent_menu.setTitle("Agent");
|
||||
conv_menu = QMenu.new(QWidget{ .ptr = null });
|
||||
conv_menu.setTitle("Conversation");
|
||||
populateAgentMenu();
|
||||
populateConvMenu();
|
||||
conv_menu.onAboutToShow(onConvMenuShow);
|
||||
|
||||
const tray_menu = QMenu.new(QWidget{ .ptr = null });
|
||||
_ = tray_menu.addMenu(agent_menu);
|
||||
_ = tray_menu.addMenu(conv_menu);
|
||||
_ = tray_menu.addSeparator();
|
||||
tray_menu.addAction(win_action);
|
||||
tray_menu.addAction(end_action);
|
||||
_ = tray_menu.addSeparator();
|
||||
tray_menu.addAction(quit_action);
|
||||
@@ -459,6 +656,7 @@ pub fn main(init: std.process.Init) !void {
|
||||
|
||||
var ov = overlay_mod.Overlay.create(allocator);
|
||||
overlay = ov;
|
||||
ov.window.show(); // visible from startup per Pierre
|
||||
_ = &ov;
|
||||
|
||||
// Own the whisper-server lifecycle: spawn AFTER the tray exists (setState
|
||||
@@ -491,6 +689,15 @@ pub fn main(init: std.process.Init) !void {
|
||||
if (last_user_text) |t| allocator.free(t);
|
||||
last_user_text = null;
|
||||
sentence_buf.deinit(allocator);
|
||||
if (prefs_agent_buf) |b| allocator.free(b);
|
||||
if (prefs_convo_buf) |b| allocator.free(b);
|
||||
for (agent_entries.items) |e| allocator.free(e.id);
|
||||
agent_entries.deinit(allocator);
|
||||
for (conv_entries.items) |e| {
|
||||
allocator.free(e.id);
|
||||
allocator.free(e.preview);
|
||||
}
|
||||
conv_entries.deinit(allocator);
|
||||
overlay_mod.Overlay.shutdownMem(); // static, no instance needed
|
||||
playback.shutdown();
|
||||
|
||||
|
||||
Reference in new issue
Block a user