summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/tui_app.zig64
1 files changed, 63 insertions, 1 deletions
diff --git a/src/tui_app.zig b/src/tui_app.zig
index d097225..98b94be 100644
--- a/src/tui_app.zig
+++ b/src/tui_app.zig
@@ -2008,6 +2008,12 @@ fn handleSubmittedLine(app: *App, term: *Terminal, line: []const u8, opts: RunOp
if (captured.len != 0) {
_ = try app.spawnStatus(captured);
}
+ // Drain any user message the command queued via
+ // `panto.ext.agent:submit` and drive it as a native turn. A loop,
+ // not an if: a handler firing during that turn may queue another.
+ while (try opts.agent.takeSubmission()) |blocks| {
+ try runSubmission(app, term, opts, blocks);
+ }
try app.renderNow();
return;
}
@@ -2038,6 +2044,56 @@ fn handleSubmittedLine(app: *App, term: *Terminal, line: []const u8, opts: RunOp
try app.renderNow();
}
+/// Drive one turn from user-message blocks queued by an extension
+/// (`panto.ext.agent:submit`). Mirrors the typed-line path: echo the
+/// message (fires `user_message`), honor a text override, resolve auth,
+/// drive the turn natively. Owns `queued` (allocated by `takeSubmission`
+/// with the conversation allocator): `run` adopts the block contents, the
+/// slice is freed here.
+fn runSubmission(app: *App, term: *Terminal, opts: RunOptions, queued: []panto.ContentBlock) !void {
+ const alloc = opts.agent.conversation.allocator;
+ var blocks = queued;
+ defer alloc.free(blocks);
+
+ // Echo the concatenated text blocks (a message with no text at all
+ // echoes as a placeholder).
+ var echo: std.ArrayList(u8) = .empty;
+ defer echo.deinit(app.alloc);
+ for (blocks) |b| switch (b) {
+ .Text => |t| {
+ if (echo.items.len != 0) try echo.append(app.alloc, '\n');
+ try echo.appendSlice(app.alloc, t.items);
+ },
+ else => {},
+ };
+ try app.spawnUser(if (echo.items.len != 0) echo.items else "[non-text message]"); // fires `user_message`
+
+ // A `user_message` handler override has the same authority as on a
+ // typed line: it replaces the queued message wholesale.
+ if (app.bus.takeOverride()) |t| {
+ defer app.alloc.free(t);
+ for (blocks) |*b| b.deinit(alloc);
+ alloc.free(blocks);
+ blocks = &.{}; // keep the defer safe if the realloc below fails
+ blocks = try alloc.alloc(panto.ContentBlock, 1);
+ blocks[0] = .{ .Text = try panto.textualBlockFromSlice(alloc, t) };
+ }
+
+ app.beginTurn();
+ try app.renderNow();
+ resolveAuthForTurn(app, opts, false) catch |err| {
+ // The turn never opened; the block contents are still ours.
+ for (blocks) |*b| b.deinit(alloc);
+ try app.routeError(err);
+ try app.renderNow();
+ return;
+ };
+ driveTurnBlocks(app, term, opts, blocks) catch |err| {
+ try app.routeError(err);
+ };
+ try app.renderNow();
+}
+
/// The provider name (left of `provider:alias`) currently selected.
fn currentProviderName(app: *App, opts: RunOptions) []const u8 {
const label = if (app.selectors) |c| c.model_label else opts.model_label;
@@ -2115,7 +2171,13 @@ fn driveTurn(app: *App, term: *Terminal, opts: RunOptions, message_text: []const
var blocks = [_]panto.ContentBlock{
.{ .Text = try panto.textualBlockFromSlice(app.alloc, message_text) },
};
- var stream = try opts.agent.run(.{ .blocks = &blocks });
+ return driveTurnBlocks(app, term, opts, &blocks);
+}
+
+/// `driveTurn` for a pre-built user message. `run` adopts the block
+/// contents; the slice itself stays the caller's.
+fn driveTurnBlocks(app: *App, term: *Terminal, opts: RunOptions, blocks: []panto.ContentBlock) !void {
+ var stream = try opts.agent.run(.{ .blocks = blocks });
defer stream.deinit();
// Two single-shot fallbacks can re-open the SAME turn (no user-message
// duplication):