Commit d2c254f

mo khan <mo@mokhan.ca>
2026-01-27 20:46:26
feat: add support for `/context <entry>`
1 parent 3150668
lib/elelem/plugins/builtins.rb
@@ -8,8 +8,30 @@ Elelem::Plugins.register(:builtins) do |agent|
     agent.terminal.say "  → context cleared"
   end
 
-  agent.commands.register("context", description: "Show conversation context") do
-    agent.terminal.say JSON.pretty_generate(agent.context)
+  agent.commands.register("context", description: "Show conversation context") do |args|
+    messages = agent.context
+
+    case args
+    when nil, ""
+      messages.each_with_index do |msg, i|
+        role = msg[:role]
+        preview = msg[:content].to_s.lines.first&.strip&.slice(0, 60) || ""
+        preview += "..." if msg[:content].to_s.length > 60
+        agent.terminal.say "  #{i + 1}. #{role}: #{preview}"
+      end
+    when "json"
+      agent.terminal.say JSON.pretty_generate(messages)
+    when /^\d+$/
+      index = args.to_i - 1
+      if index >= 0 && index < messages.length
+        content = messages[index][:content].to_s
+        agent.terminal.say(agent.terminal.markdown(content))
+      else
+        agent.terminal.say "  Invalid index: #{args}"
+      end
+    else
+      agent.terminal.say "  Usage: /context [json|<number>]"
+    end
   end
 
   agent.commands.register("shell", description: "Start interactive shell") do
lib/elelem/templates/system_prompt.erb
@@ -1,49 +0,0 @@
-Terminal coding agent. Be concise. Verify your work.
-
-# Tools
-- read(path): file contents
-- write(path, content): create/overwrite file
-- execute(command): shell command
-- eval(ruby): execute Ruby code; use to create tools for repetitive tasks
-- task(prompt): delegate complex searches or multi-file analysis to a focused subagent
-
-# Editing
-Use execute(`patch -p1`) for multi-line changes: `echo "DIFF" | patch -p1`
-Use execute(`sed`) for single-line changes: `sed -i'' 's/old/new/' file`
-Use write for new files or full rewrites
-
-# Search
-Use execute(`rg`) for text search: `rg -n "pattern" .`
-Use execute(`fd`) for file discovery: `fd -e rb .`
-Use execute(`sg`) (ast-grep) for structural search: `sg -p 'def $NAME' -l ruby`
-
-# Task Management
-For complex tasks:
-1. State plan before acting
-2. Work through steps one at a time
-3. Summarize what was done
-
-# Long Tasks
-For complex multi-step work, write notes to .elelem/scratch.md
-
-# Policy
-- Explain before non-trivial commands
-- Verify changes (read file, run tests)
-- No interactive flags (-i, -p)
-- Use `man` when you need to understand how to execute a program
-
-# Environment
-pwd: <%= pwd %>
-platform: <%= platform %>
-date: <%= date %>
-Reasoning: high
-self (this agent's source): <%= elelem_source %>
-<%= git_branch %>
-
-# Codebase
-<%= repo_map %>
-<% if agents_md %>
-
-# Project Instructions
-<%= agents_md %>
-<% end %>
lib/elelem/agent.rb
@@ -25,8 +25,9 @@ module Elelem
     end
 
     def command(input)
-      name = input.delete_prefix("/")
-      commands.run(name) || terminal.say(commands.names.join(" "))
+      parts = input.delete_prefix("/").split(" ", 2)
+      name, args = parts[0], parts[1]
+      commands.run(name, args) || terminal.say(commands.names.join(" "))
     end
 
     def context
lib/elelem/commands.rb
@@ -10,11 +10,11 @@ module Elelem
       @registry[name] = { description: description, handler: handler }
     end
 
-    def run(name)
+    def run(name, args = nil)
       entry = @registry[name]
       return false unless entry
 
-      entry[:handler].call
+      entry[:handler].arity == 0 ? entry[:handler].call : entry[:handler].call(args)
       true
     end
 
lib/elelem/system_prompt.rb
@@ -2,80 +2,145 @@
 
 module Elelem
   class SystemPrompt
-    TEMPLATE_PATH = File.expand_path("templates/system_prompt.erb", __dir__)
+    TEMPLATE = <<~ERB
+      Terminal coding agent. Be concise. Verify your work.
+
+      # Tools
+      - read(path): file contents
+      - write(path, content): create/overwrite file
+      - execute(command): shell command
+      - eval(ruby): execute Ruby code; use to create tools for repetitive tasks
+      - task(prompt): delegate complex searches or multi-file analysis to a focused subagent
+
+      # Editing
+      Use execute(`patch -p1`) for multi-line changes: `echo "DIFF" | patch -p1`
+      Use execute(`sed`) for single-line changes: `sed -i'' 's/old/new/' file`
+      Use write for new files or full rewrites
+
+      # Search
+      Use execute(`rg`) for text search: `rg -n "pattern" .`
+      Use execute(`fd`) for file discovery: `fd -e rb .`
+      Use execute(`sg`) (ast-grep) for structural search: `sg -p 'def $NAME' -l ruby`
+
+      # Task Management
+      For complex tasks:
+      1. State plan before acting
+      2. Work through steps one at a time
+      3. Summarize what was done
+
+      # Long Tasks
+      For complex multi-step work, write notes to .elelem/scratch.md
+
+      # Policy
+      - Explain before non-trivial commands
+      - Verify changes (read file, run tests)
+      - No interactive flags (-i, -p)
+      - Use `man` when you need to understand how to execute a program
+
+      # Environment
+      pwd: <%= pwd %>
+      platform: <%= platform %>
+      date: <%= date %>
+      self: <%= elelem_source %>
+      <%= git_info %>
+
+      <% if repo_map && !repo_map.empty? %>
+      # Codebase
+      ```
+      <%= repo_map %>```
+      <% end %>
+      <%= agents_md %>
+    ERB
 
     def render
-      ERB.new(template, trim_mode: "-").result(binding)
+      ERB.new(TEMPLATE, trim_mode: "-").result(binding)
     end
 
     private
 
-    def template
-      File.read(TEMPLATE_PATH)
-    end
-
-    def pwd
-      Dir.pwd
-    end
+    def pwd = Dir.pwd
+    def platform = RUBY_PLATFORM.split("-").last
+    def date = Date.today
 
     def elelem_source
-      File.expand_path("../..", __dir__)
-    end
-
-    def platform
-      RUBY_PLATFORM.split("-").last
-    end
-
-    def date
-      Date.today
+      spec = Gem.loaded_specs["elelem"]
+      spec ? spec.gem_dir : File.expand_path("../..", __dir__)
     end
 
-    def git_branch
+    def git_info
       return unless File.exist?(".git")
-
       "branch: #{`git branch --show-current`.strip}"
     rescue Errno::ENOENT
       nil
     end
 
     def repo_map
-      symbols = extract_with_sg
-      return ctags_fallback if symbols.nil?
+      files = `git ls-files '*.rb' 2>/dev/null`.lines.map(&:strip)
+      return "" if files.empty?
 
+      symbols = extract_symbols(files)
       format_symbols(symbols, budget: 2000)
     end
 
-    def extract_with_sg
+    def extract_symbols(files)
       output = `sg run -p 'def $NAME' -l ruby --json=compact . 2>/dev/null`
-      return nil unless $?.success?
-
-      JSON.parse(output).map do |match|
-        {
-          file: match["file"],
-          name: match.dig("metaVariables", "single", "NAME", "text")
-        }
-      end.reject { |m| m[:file].include?("spec/") || m[:file].include?("vendor/") }
-    rescue Errno::ENOENT, JSON::ParserError
-      nil
+      return ctags_fallback(files) unless $?.success?
+
+      parse_sg_output(output, files)
     end
 
-    def format_symbols(symbols, budget:)
-      result = String.new
-      symbols.group_by { |s| s[:file] }.each do |file, syms|
-        line = "#{file}: #{syms.map { |s| s[:name] }.uniq.join(", ")}\n"
-        break if result.length + line.length > budget
-        result << line
+    def parse_sg_output(output, tracked_files)
+      JSON.parse(output).filter_map do |match|
+        file = match["file"]
+        next unless tracked_files.include?(file)
+        { file: file, name: match.dig("metaVariables", "single", "NAME", "text") }
       end
-      result
+    rescue JSON::ParserError
+      []
     end
 
-    def ctags_fallback
-      symbols = `ctags -x --sort=no --languages=Ruby --kinds-Ruby=cfS --exclude=spec --exclude=vendor -R . 2>/dev/null`.lines
-        .map { |l| parts = l.split(/\s+/, 4); {file: parts[3]&.split&.first, name: parts[0]} }
+    def ctags_fallback(files)
+      output = `ctags -x --languages=Ruby --kinds-Ruby=cfm -L - 2>/dev/null <<< "#{files.join("\n")}"`
+      return [] unless $?.success?
 
-      format_symbols(symbols, budget: 2000)
+      output.lines.map do |line|
+        parts = line.split(/\s+/, 4)
+        { file: parts[3]&.split&.first, name: parts[0] }
+      end
     rescue Errno::ENOENT
-      ""
+      []
+    end
+
+    def format_symbols(symbols, budget:)
+      tree = build_tree(symbols)
+      render_tree(tree, budget: budget)
+    end
+
+    def build_tree(symbols)
+      tree = {}
+      symbols.group_by { |s| s[:file] }.each do |file, syms|
+        parts = file.split("/")
+        node = tree
+        parts[0..-2].each { |dir| node = (node[dir + "/"] ||= {}) }
+        node[parts.last] = syms.map { |s| s[:name] }.uniq
+      end
+      tree
+    end
+
+    def render_tree(node, indent: 0, budget:, result: String.new)
+      node.each do |key, value|
+        if value.is_a?(Hash)
+          line = "  " * indent + key + "\n"
+          return result if result.length + line.length > budget
+          result << line
+          render_tree(value, indent: indent + 1, budget: budget, result: result)
+        else
+          line = "  " * indent + key.sub(/\.rb$/, "") + ": " + value.join(" ") + "\n"
+          return result if result.length + line.length > budget
+          result << line
+        end
+      end
+      result
     end
 
     def agents_md