Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ Example plugins in [`plugins/`](plugins/):
| `local_openai.janet` | `harness/register-provider` declaring vLLM/Ollama/LMStudio local endpoints |
| `session_tree.janet` | `harness/set-label` + `harness/new-session` — `/label` and `/fresh` slash commands |
| `turn_timer/` | Multi-file plugin — state, hooks, and a `/timer-stats` command split across three files in a single directory |
| `response_inspector.janet` | `on-response` hook — pattern-match the LLM's reply, post notifications, and return a steering string appended to the next turn's system prompt |

## LSP integration

Expand Down
51 changes: 51 additions & 0 deletions plugins/response_inspector.janet
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Response inspector example
#
# Demonstrates the `on-response` hook — the host calls it after every
# LLM response with `(ctx :response)` set to the full assistant
# message text. This hook is the natural place to:
#
# 1. Inspect the response for patterns and POST NOTIFICATIONS via
# `harness/notify` (chat-visible, fire-and-forget).
# 2. Return a string to be APPENDED to the system prompt on the
# next turn — useful for steering subsequent responses based on
# patterns in this one (e.g. "the model is being terse, ask it
# to be more thorough next time").
# 3. Inspect tool calls embedded in the response by piping through
# `on-tool-end` (which fires PER tool call and is the right
# place for `harness/replace-result` if you want to rewrite the
# tool output the LLM sees).
#
# This example posts a notification when the response contains a
# code block, and gently nudges the model to add more comments if
# the previous response had unannotated code.

(def hooks [])

(var last-had-code-block false)

(defn response_inspector-on-response [ctx]
(let [text (or (ctx :response) "")]
# 1. Notification when a response includes a code block.
(when (string/find "```" text)
(harness/notify "agent included a code block" :info)
(set last-had-code-block true))

# 2. Steering string: if the prior response had code but no
# `;` or `//` comment lines, append a system-prompt hint for
# the next turn asking for more annotations.
(if (and last-had-code-block
(string/find "```" text)
(not (or (string/find ";; " text)
(string/find "// " text)
(string/find "# " text))))
(do
(set last-had-code-block false)
# The returned string is appended to the system prompt
# injection for the next turn.
(string "The previous response included code but no inline "
"comments. When writing code, briefly annotate the "
"*why* of non-obvious lines (one short comment per "
"non-trivial block)."))
(do
(set last-had-code-block false)
nil))))
52 changes: 27 additions & 25 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -618,32 +618,34 @@ pub async fn run_interactive(
)?;
continue;
}
UserEvent::MouseDown { row, col: _ } => {
UserEvent::MouseDown { row, col } => {
if row < renderer.visible_lines() as u16
&& let Some(idx) = renderer.buffer_line_at_row(row) {
renderer.selection_active = true;
renderer.selection_start = Some(idx);
renderer.selection_end = Some(idx);
renderer.render_viewport()?;
renderer.draw_bottom(
&input,
&with_queue(StatusLine::render(session, is_running, 0, loop_label.as_deref(), context.current_prompt_name.as_deref(), perm_mode().as_deref()), interjection_queue.len()),
is_running,
)?;
}
&& let Some(pos) = renderer.buffer_pos_at(row, col)
{
renderer.selection_active = true;
renderer.selection_start = Some(pos);
renderer.selection_end = Some(pos);
renderer.render_viewport()?;
renderer.draw_bottom(
&input,
&with_queue(StatusLine::render(session, is_running, 0, loop_label.as_deref(), context.current_prompt_name.as_deref(), perm_mode().as_deref()), interjection_queue.len()),
is_running,
)?;
}
continue;
}
UserEvent::MouseDrag { row, col: _ } => {
UserEvent::MouseDrag { row, col } => {
if renderer.selection_active
&& let Some(idx) = renderer.buffer_line_at_row(row) {
renderer.selection_end = Some(idx);
renderer.render_viewport()?;
renderer.draw_bottom(
&input,
&with_queue(StatusLine::render(session, is_running, 0, loop_label.as_deref(), context.current_prompt_name.as_deref(), perm_mode().as_deref()), interjection_queue.len()),
is_running,
)?;
}
&& let Some(pos) = renderer.buffer_pos_at(row, col)
{
renderer.selection_end = Some(pos);
renderer.render_viewport()?;
renderer.draw_bottom(
&input,
&with_queue(StatusLine::render(session, is_running, 0, loop_label.as_deref(), context.current_prompt_name.as_deref(), perm_mode().as_deref()), interjection_queue.len()),
is_running,
)?;
}
continue;
}
UserEvent::Paste(text) => {
Expand All @@ -668,10 +670,10 @@ pub async fn run_interactive(
)?;
continue;
}
UserEvent::MouseUp { row, col: _ } => {
UserEvent::MouseUp { row, col } => {
if renderer.selection_active {
if let Some(idx) = renderer.buffer_line_at_row(row) {
renderer.selection_end = Some(idx);
if let Some(pos) = renderer.buffer_pos_at(row, col) {
renderer.selection_end = Some(pos);
}
if let Some(text) = renderer.selected_text() {
copy_to_clipboard(&text);
Expand Down
Loading
Loading