repl: @path mention expansion in input lines (closes #7)
Saves the user from manual copy/paste: typing "show me @repl.lua" or
"compare @config.lua and @config.example.lua" auto-expands each mention
to a fenced code block carrying the file contents, language-tagged by
extension, and feeds the composed text to the broker.
Wired on the "ai" branch of the input loop and inside :ask. Meta and
shell branches pass through unchanged — "@foo" in shell context is a
literal program argument; meta commands store text verbatim.
Trigger rule: "@" must follow start-of-string or whitespace — avoids
false positives on email addresses ("user@example.com") and shell
short-options. Path extends to next whitespace.
Other behavior:
- Language tag derived from extension via a small lookup; unknown
extensions yield an untagged fence.
- Files over 32 KB are truncated head/tail (16K + 8K) with a marker.
- Missing files leave the literal "@path" token in place and emit
a "[aish] @path: not found" status — non-fatal, lets the user
correct the path and re-type.
- Each successful expansion emits "[aish] @path expanded (N bytes
[, truncated])" so the user sees what was inlined.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,77 @@ local mcp = require("mcp")
|
|||||||
local safety = require("safety")
|
local safety = require("safety")
|
||||||
local json = require("dkjson")
|
local json = require("dkjson")
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------- @-mentions (issue #7)
|
||||||
|
-- Triggered when "@" follows start-of-string or whitespace (avoids
|
||||||
|
-- false positives on email addresses like user@example.com). Path
|
||||||
|
-- runs until next whitespace. The mention is replaced by a fenced
|
||||||
|
-- code block carrying the file contents, language-tagged by extension.
|
||||||
|
-- Files over MENTION_MAX_BYTES are truncated head+tail with a marker.
|
||||||
|
local MENTION_MAX_BYTES = 32 * 1024
|
||||||
|
local MENTION_HEAD = 16 * 1024
|
||||||
|
local MENTION_TAIL = 8 * 1024
|
||||||
|
local LANG_BY_EXT = {
|
||||||
|
lua = "lua", py = "python", js = "javascript", ts = "typescript",
|
||||||
|
sh = "bash", c = "c", h = "c", cc = "cpp", cpp = "cpp", hpp = "cpp",
|
||||||
|
rs = "rust", go = "go", java = "java", rb = "ruby", md = "markdown",
|
||||||
|
json = "json", yaml = "yaml", yml = "yaml", toml = "toml",
|
||||||
|
html = "html", css = "css", sql = "sql", xml = "xml",
|
||||||
|
}
|
||||||
|
local function _lang_of(path)
|
||||||
|
local ext = path:match("%.([%w]+)$")
|
||||||
|
return ext and LANG_BY_EXT[ext:lower()] or ""
|
||||||
|
end
|
||||||
|
local function _read_truncated(path)
|
||||||
|
local f = io.open(path, "rb")
|
||||||
|
if not f then return nil end
|
||||||
|
local content = f:read("*a") or ""
|
||||||
|
f:close()
|
||||||
|
if #content <= MENTION_MAX_BYTES then return content, false end
|
||||||
|
local head = content:sub(1, MENTION_HEAD)
|
||||||
|
local tail = content:sub(#content - MENTION_TAIL + 1)
|
||||||
|
return head
|
||||||
|
.. ("\n... [%d bytes elided] ...\n"):format(#content - MENTION_HEAD - MENTION_TAIL)
|
||||||
|
.. tail, true
|
||||||
|
end
|
||||||
|
local function expand_mentions(line, on_status)
|
||||||
|
-- Walk the line; for each "@<path>" preceded by SOL or whitespace,
|
||||||
|
-- attempt to read and substitute. Missing files leave the literal
|
||||||
|
-- token in place + emit a status warning.
|
||||||
|
local out, i = {}, 1
|
||||||
|
while i <= #line do
|
||||||
|
local at_start = (i == 1) or line:sub(i - 1, i - 1):match("%s") ~= nil
|
||||||
|
if at_start and line:sub(i, i) == "@" then
|
||||||
|
local path_end = line:find("%s", i + 1) or (#line + 1)
|
||||||
|
local path = line:sub(i + 1, path_end - 1)
|
||||||
|
if path ~= "" then
|
||||||
|
local content, truncated = _read_truncated(path)
|
||||||
|
if content then
|
||||||
|
if on_status then
|
||||||
|
on_status(("@%s expanded (%d bytes%s)"):format(
|
||||||
|
path, #content, truncated and ", truncated" or ""))
|
||||||
|
end
|
||||||
|
out[#out + 1] = ("```%s path=%s\n%s\n```"):format(
|
||||||
|
_lang_of(path), path, content)
|
||||||
|
i = path_end
|
||||||
|
else
|
||||||
|
if on_status then
|
||||||
|
on_status(("@%s: not found"):format(path))
|
||||||
|
end
|
||||||
|
out[#out + 1] = line:sub(i, path_end - 1)
|
||||||
|
i = path_end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
out[#out + 1] = "@"
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
out[#out + 1] = line:sub(i, i)
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return table.concat(out)
|
||||||
|
end
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local HELP = [[
|
local HELP = [[
|
||||||
@@ -24,7 +95,7 @@ Meta commands:
|
|||||||
:models list configured models (* = active)
|
:models list configured models (* = active)
|
||||||
:history show conversation turns
|
:history show conversation turns
|
||||||
:exec <cmd> force shell execution
|
:exec <cmd> force shell execution
|
||||||
:ask <text> force AI query
|
:ask <text> force AI query (supports @path expansion)
|
||||||
:sessions list session log files
|
:sessions list session log files
|
||||||
:save <name> rename current session log to <name>.jsonl
|
:save <name> rename current session log to <name>.jsonl
|
||||||
:resume <name> load <name>.jsonl turns into the in-memory context
|
:resume <name> load <name>.jsonl turns into the in-memory context
|
||||||
@@ -783,7 +854,7 @@ function M.run(config)
|
|||||||
ask = function(args)
|
ask = function(args)
|
||||||
args = (args or ""):match("^%s*(.-)%s*$")
|
args = (args or ""):match("^%s*(.-)%s*$")
|
||||||
if args == "" then renderer.status("usage: :ask <text>"); return end
|
if args == "" then renderer.status("usage: :ask <text>"); return end
|
||||||
ask_ai(args)
|
ask_ai(expand_mentions(args, renderer.status))
|
||||||
end,
|
end,
|
||||||
sessions = function()
|
sessions = function()
|
||||||
if not sessions_dir then renderer.status("(no history.dir configured)"); return end
|
if not sessions_dir then renderer.status("(no history.dir configured)"); return end
|
||||||
@@ -1210,7 +1281,8 @@ function M.run(config)
|
|||||||
elseif kind == "shell" then
|
elseif kind == "shell" then
|
||||||
run_shell(payload)
|
run_shell(payload)
|
||||||
else -- "ai"
|
else -- "ai"
|
||||||
ask_ai(payload)
|
local expanded = expand_mentions(payload, renderer.status)
|
||||||
|
ask_ai(expanded)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user