L
lingcoder
↓ 0 downloadsLingCoder: a Geany / VS Code-style IDE for Ling, written in Ling. Explorer, tabs, syntax-coloured editor with lint, find in files, command palette, run/build panel, and hot-reloaded extensions - including a Claude assistant.
Keywords ide, editor, ling, claude, extensions
lingfu install lingcoderCode / Files (34)
extensions/claude/claude.ling
# claude.ling - Claude inside LingCoder: a chat panel (and a full chat tab), quick actions
# on the selection (Ctrl+K), streaming replies, Insert/Copy on code blocks, and TOOLS:
# Claude can read/list/open files, write files and run commands - each run/write waits
# for your Allow/Deny in the panel. Commands are platform-aware (cmd.exe on Windows,
# sh elsewhere, no shell at all on LingOS) and `miao ...` is routed to the miao VCS
# launcher so Ling temples never get git.
#
# Transport: curl in a background job; the API key rides in a local curl config file,
# never on the command line. Set it in Settings (Ctrl+,) or ANTHROPIC_API_KEY.
# History entries in st list "cl_msgs": "u|text" "a|text"
# "A|<display>@@RAW@@<assistant content JSON>" (a reply that called tools)
# "T|<display>@@RAW@@<tool_result content JSON>" (the results sent back)
fn claude_info() { return ["Claude", "Chat about your code - explain, fix, refactor - and let Claude run tools with your permission", "2.0"] }
fn claude_panel_info() { return ["Claude - ask about your code (Ctrl+K)", "brain"] }
fn claude_commands() {
return ["open|Claude: Open Chat|Ctrl+K|brain", "tab|Claude: Open Chat as Tab||window", "ask_selection|Claude: Ask About Selection|Ctrl+K|brain", "explain|Claude: Explain Current File||brain", "fix|Claude: Fix the Problems||brain", "refactor|Claude: Refactor Selection||brain", "comment|Claude: Comment Selection||brain", "stop|Claude: Stop Reply||stop", "clear|Claude: Clear Chat||trash"]
}
fn claude_ctx_edit() {
return ["Ask Claude About Selection|ext:claude:ask_selection||brain", "Refactor With Claude|ext:claude:refactor||brain", "Explain With Claude|ext:claude:explain_sel||brain"]
}
fn claude_show() {
if st_get("ptab_on", 0.0) > 0.5 && st_get("ptab_stem", "") == "claude" { st_set("ptab_active", 1.0) }
else { side_show("ext:claude") }
st_set("focus", "cl_in")
st_set("text_owner", "")
}
fn claude_run(id) {
if id == "open" { claude_show() return "" }
if id == "tab" {
ide_open_panel_tab("claude", "Claude Chat", "brain")
st_set("focus", "cl_in")
st_set("text_owner", "")
return ""
}
if id == "ask_selection" {
claude_show()
if ide_selection() != "" {
st_set("cl_ctx_sel", 1.0)
claude_send("Explain this selected code, briefly. If something is wrong with it, say so and show the fix.")
}
return ""
}
if id == "explain" {
claude_show()
st_set("cl_ctx_file", 1.0)
claude_send("Explain what this file does: a short overview, then the important parts.")
return ""
}
if id == "explain_sel" {
claude_show()
st_set("cl_ctx_sel", 1.0)
claude_send("Explain the selected code.")
return ""
}
if id == "fix" {
claude_show()
st_set("cl_ctx_file", 1.0)
st_set("cl_ctx_prob", 1.0)
claude_send("Fix the problems listed. Show only the corrected lines (with their line numbers) unless more context is needed.")
return ""
}
if id == "refactor" {
claude_show()
st_set("cl_ctx_sel", 1.0)
claude_send("Refactor the selected code: simpler and clearer, same behaviour, same style. Reply with one ling code block I can paste over the selection, then one sentence on what changed.")
return ""
}
if id == "comment" {
claude_show()
st_set("cl_ctx_sel", 1.0)
claude_send("Add short, useful comments to the selected code. Reply with one ling code block I can paste over the selection.")
return ""
}
if id == "stop" { claude_stop() return "Stopped" }
if id == "clear" {
if claude_busy() { claude_stop() }
st_lclear("cl_msgs")
st_set("cl_err", "")
st_set("cl_rounds", 0.0)
return "Chat cleared"
}
return ""
}
# ── environment awareness ─────────────────────────────────────────────────────
fn claude_can_shell() { return plat_name() == "desktop" }
fn claude_shell_name() {
if !claude_can_shell() { return "none" }
if plat_os_name() == "windows" { return "cmd.exe" }
return "sh"
}
fn claude_miao_home() {
bind r = script_call("miao_home", [])
if list_get(r, 0) == true { return "" + list_get(r, 1) }
return ""
}
fn claude_has_miao() {
bind r = script_call("miao_repo_root", [])
if list_get(r, 0) == true { return "" + list_get(r, 1) != "" }
return false
}
fn claude_system() {
bind s = "You are Claude, built into LingCoder, an IDE for the Ling programming language (the IDE itself is written in Ling). Be concise and concrete. Put code in fenced blocks (```), one block per paste-able piece."
bind s = s + " Ling essentials, so your Ling code is valid: functions `fn name(a, b) { ... }`; variables ONLY with `bind x = 1.0` (no let/var/const; reassign by binding again); every number is a float like 1.0; no unary minus - write `0.0 - x`; loops are `while cond { }` (no for); `if cond { } else if { } else { }`; strings with double quotes, concatenation with +; lists `[a, b]` with list_get/list_push/len (lists are values: rebind the result); NO maps/dicts (use the st_set/st_get store or parallel lists); comments start with #; comparisons ==, !=, <, >; booleans true/false with && || !; the entry point of a program is `bind start = do { ... }`; modules import with `use \"file.ling\"`."
# the machine this session runs on
bind s = s + " Environment: platform " + plat_name() + ", OS " + plat_os_name() + " (" + plat_arch() + ")."
if proj_root() != "" { bind s = s + " The open project folder is " + proj_root() + "; tool paths may be relative to it." }
if claude_can_shell() {
if plat_os_name() == "windows" {
bind s = s + " run_command runs through cmd.exe on Windows: use dir, type, copy, del, mkdir, && chaining and backslash paths - NOT bash syntax (no ls, cat, rm, export, $VAR)."
} else {
bind s = s + " run_command runs through sh."
}
} else {
bind s = s + " This platform (LingOS) has NO shell and no processes: there is no run_command here, only the file tools. Never suggest shell commands as if they could run."
}
# miao, the Ling world's version control
bind s = s + " Version control: Ling projects use miao (the temple of versions), NOT git. A project with a .miao folder is a miao repository" + sel(claude_has_miao(), " - THIS project is one", "") + ". In run_command, write miao commands as `miao <ceremony> ...` and the IDE routes them to the miao launcher. Never run git in a miao repository. Ceremonies (git name in brackets): divine [status], offer <paths|.> [add], withdraw <paths> [unstage], rite -m \"msg\" [commit], lineage [-n N] [log], yinyang [diff], soul [name|-d name] [branch], summon <soul|rite> [switch], harmonize <soul> [merge], talisman [name] [tag], manifest [rite|path] [show], restore <paths>, pilgrimage [pull], tribute [push], enshrine <link> [clone], mother [remote], chronicle [reflog], keeper [config]."
if claude_tools_on() { bind s = s + " Prefer reading files and running read-only commands over asking the user; ask before anything destructive. After using tools, summarise what you found or did." }
return s
}
fn claude_tools_on() { return st_get("cl_tools", 1.0) > 0.5 }
fn claude_tools_json() {
bind t = "["
if claude_can_shell() {
bind dialect = sel(plat_os_name() == "windows", "cmd.exe on Windows (dir, type, copy, del, && - not bash)", "sh")
bind t = t + "{\"name\":\"run_command\",\"description\":\"Run one command line in the project folder via " + dialect + ". The user approves each run and sees the output. miao version-control commands work here: write them as miao <ceremony> and the IDE calls the real launcher.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"the exact command line\"}},\"required\":[\"command\"]}},"
}
bind t = t + "{\"name\":\"read_file\",\"description\":\"Read a text file. Paths resolve against the project root; reads inside the project need no approval.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}},\"required\":[\"path\"]}},"
bind t = t + "{\"name\":\"write_file\",\"description\":\"Create or overwrite a text file (the user approves, seeing path and size).\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"},\"content\":{\"type\":\"string\"}},\"required\":[\"path\",\"content\"]}},"
bind t = t + "{\"name\":\"list_dir\",\"description\":\"List a folder (name, kind, size). Paths resolve against the project root.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}},\"required\":[\"path\"]}},"
bind t = t + "{\"name\":\"open_file\",\"description\":\"Open a file in an editor tab so the user sees it.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}},\"required\":[\"path\"]}}"
return t + "]"
}
# ── transport ─────────────────────────────────────────────────────────────────
fn claude_busy() { return st_get("cl_job", 0.0 - 1.0) >= 0.0 || st_get("cl_phase", "") == "tools" }
fn claude_log() { return p_join(cache_dir(), "claude_sse.log") }
fn claude_context() {
bind s = ""
if st_get("cl_ctx_file", 1.0) > 0.5 && ide_cur_path() != "" {
bind body = ide_cur_text()
if str_len(body) > 12000.0 { bind body = str_slice(body, 0.0, 12000.0) + "\n... (file truncated)" }
bind s = s + "\n\n<current_file path=\"" + ide_cur_path() + "\">\n" + body + "\n</current_file>"
}
if st_get("cl_ctx_sel", 0.0) > 0.5 && ide_selection() != "" {
bind s = s + "\n\n<selection>\n" + ide_selection() + "\n</selection>"
}
if st_get("cl_ctx_prob", 0.0) > 0.5 && ide_problems_text() != "" {
bind s = s + "\n\n<problems>\n" + ide_problems_text() + "</problems>"
}
return s
}
# the request body from the history (entries described in the header)
fn claude_build_body() {
bind body = "{\"model\":\"" + claude_model() + "\",\"max_tokens\":" + num_str(st_get("claude_tokens", 4096.0)) + ",\"stream\":true"
bind body = body + ",\"system\":\"" + json_escape(claude_system()) + "\""
if claude_tools_on() { bind body = body + ",\"tools\":" + claude_tools_json() }
bind body = body + ",\"messages\":["
bind n = st_llen("cl_msgs")
bind from = max(0.0, n - 16.0)
while from < n && !str_starts_with(st_lget("cl_msgs", from), "u|") { bind from = from + 1.0 }
# the last plain user entry carries the context block
bind lastu = 0.0 - 1.0
bind i = from
while i < n {
if str_starts_with(st_lget("cl_msgs", i), "u|") { bind lastu = i }
bind i = i + 1.0
}
bind i = from
bind first = true
while i < n {
bind m = st_lget("cl_msgs", i)
bind piece = ""
if str_starts_with(m, "u|") {
bind text = str_slice(m, 2.0, str_len(m) - 2.0)
if i == lastu { bind text = text + claude_context() }
bind piece = "{\"role\":\"user\",\"content\":\"" + json_escape(text) + "\"}"
} else if str_starts_with(m, "a|") {
bind piece = "{\"role\":\"assistant\",\"content\":\"" + json_escape(str_slice(m, 2.0, str_len(m) - 2.0)) + "\"}"
} else if str_starts_with(m, "A|") {
bind piece = "{\"role\":\"assistant\",\"content\":" + claude_raw_of(m) + "}"
} else if str_starts_with(m, "T|") {
bind piece = "{\"role\":\"user\",\"content\":" + claude_raw_of(m) + "}"
}
if piece != "" {
if !first { bind body = body + "," }
bind body = body + piece
bind first = false
}
bind i = i + 1.0
}
return body + "]}"
}
fn claude_raw_of(entry) {
bind parts = str_split(entry, "@@RAW@@")
if len(parts) < 2.0 { return "[]" }
return list_get(parts, 1)
}
fn claude_display_of(entry) {
bind body = str_slice(entry, 2.0, str_len(entry) - 2.0)
bind parts = str_split(body, "@@RAW@@")
return list_get(parts, 0)
}
fn claude_send(usertext) {
if str_trim(usertext) == "" { return 0.0 }
if claude_busy() { ide_status("Claude is already replying - Stop it first") return 0.0 }
st_lpush("cl_msgs", "u|" + usertext)
st_set("cl_rounds", 0.0)
claude_request()
return 0.0
}
fn claude_request() {
bind key = claude_api_key()
if key == "" {
st_set("cl_err", "No API key: set one in Settings (Ctrl+,) or the ANTHROPIC_API_KEY environment variable.")
return 0.0
}
st_set("cl_err", "")
bind reqp = p_join(cache_dir(), "claude_req.json")
plat_write(reqp, claude_build_body())
bind cfg = "silent\nshow-error\nno-buffer\n"
bind cfg = cfg + "url = \"https://api.anthropic.com/v1/messages\"\n"
bind cfg = cfg + "header = \"x-api-key: " + key + "\"\n"
bind cfg = cfg + "header = \"anthropic-version: 2023-06-01\"\n"
bind cfg = cfg + "header = \"content-type: application/json\"\n"
bind cfg = cfg + "data-binary = \"@" + reqp + "\"\n"
bind cfgp = p_join(cache_dir(), "claude_curl.cfg")
plat_write(cfgp, cfg)
bind h = plat_job_start("curl", ["-K", cfgp], cache_dir(), claude_log())
if h < 0.0 {
st_set("cl_err", "Could not start curl (is curl.exe on the PATH? Windows 10+ ships it).")
return 0.0
}
st_set("cl_job", h)
st_set("cl_phase", "stream")
st_set("cl_live", "")
st_set("cl_t0", st_get("now", 0.0))
st_set("ext_busy_claude", 1.0)
st_set("cl_stick", 1.0)
ide_status("Asking " + claude_model() + " ...")
return 0.0
}
fn claude_continue() {
bind r = st_get("cl_rounds", 0.0) + 1.0
st_set("cl_rounds", r)
if r > 8.0 {
st_set("cl_err", "Stopped after 8 tool rounds in one turn - send a message to continue.")
st_set("cl_phase", "")
st_set("ext_busy_claude", 0.0)
return 0.0
}
claude_request()
return 0.0
}
fn claude_stop() {
if st_get("cl_job", 0.0 - 1.0) >= 0.0 {
plat_job_kill(st_get("cl_job", 0.0 - 1.0))
st_set("cl_job", 0.0 - 1.0)
if st_get("cl_phase", "") == "stream" {
bind partial = st_get("cl_live", "")
if partial != "" { st_lpush("cl_msgs", "a|" + partial + "\n(stopped)") }
}
}
if st_get("cl_phase", "") == "tools" { claude_tools_cancel() }
st_set("cl_phase", "")
st_set("cl_live", "")
st_set("ext_busy_claude", 0.0)
return 0.0
}
# ── small JSON helpers (values stay escaped until claude_unesc) ───────────────
fn claude_unesc(s) {
bind out = ""
bind n = str_len(s)
bind i = 0.0
while i < n {
bind ch = str_slice(s, i, 1.0)
if ch == "\\" && i + 1.0 < n {
bind c2 = str_slice(s, i + 1.0, 1.0)
if c2 == "n" { bind out = out + "\n" bind i = i + 2.0 }
else if c2 == "t" { bind out = out + " " bind i = i + 2.0 }
else if c2 == "r" { bind i = i + 2.0 }
else if c2 == "u" { bind out = out + "?" bind i = i + 6.0 }
else { bind out = out + c2 bind i = i + 2.0 }
} else {
bind out = out + ch
bind i = i + 1.0
}
}
return out
}
# the (still escaped) string value of "key":"..." in one JSON line; "" when absent
fn claude_jfield(line, key) {
bind parts = str_split(line, "\"" + key + "\":\"")
if len(parts) < 2.0 {
bind parts = str_split(line, "\"" + key + "\": \"")
if len(parts) < 2.0 { return "" }
}
bind rest = list_get(parts, 1)
bind out = ""
bind n = str_len(rest)
bind i = 0.0
bind run = true
while i < n && run {
bind ch = str_slice(rest, i, 1.0)
if ch == "\\" { bind out = out + ch + str_slice(rest, i + 1.0, 1.0) bind i = i + 2.0 }
else if ch == "\"" { bind run = false }
else { bind out = out + ch bind i = i + 1.0 }
}
return out
}
fn claude_jindex(line) {
bind at = str_find(line, "\"index\":")
if at < 0.0 { return 0.0 - 1.0 }
bind rest = str_slice(line, at + 8.0, 8.0)
bind num = ""
bind i = 0.0
while i < str_len(rest) && syn_is_digit(str_slice(rest, i, 1.0)) {
bind num = num + str_slice(rest, i, 1.0)
bind i = i + 1.0
}
if num == "" { return 0.0 - 1.0 }
return to_number(num)
}
# ── the per-frame tick: stream parsing, then the tool queue ───────────────────
fn claude_tick() {
if st_get("cl_phase", "") == "tools" { claude_tools_tick() return 0.0 }
if st_get("cl_job", 0.0 - 1.0) < 0.0 { return 0.0 }
if st_get("now", 0.0) - st_get("cl_poll", 0.0) < 0.12 { return 0.0 }
st_set("cl_poll", st_get("now", 0.0))
bind t = plat_job_log(claude_log())
bind lines = str_split(t, "\n")
bind acc = ""
bind done = false
bind tool_idx = []
bind tool_id = []
bind tool_name = []
bind tool_json = []
bind i = 0.0
while i < len(lines) {
bind ln = list_get(lines, i)
if str_find(ln, "text_delta") >= 0.0 {
bind acc = acc + claude_jfield(ln, "text")
} else if str_find(ln, "content_block_start") >= 0.0 && str_find(ln, "\"tool_use\"") >= 0.0 {
bind tool_idx = list_push(tool_idx, claude_jindex(ln))
bind tool_id = list_push(tool_id, claude_unesc(claude_jfield(ln, "id")))
bind tool_name = list_push(tool_name, claude_unesc(claude_jfield(ln, "name")))
bind tool_json = list_push(tool_json, "")
} else if str_find(ln, "input_json_delta") >= 0.0 {
bind ix = claude_jindex(ln)
bind k = 0.0
while k < len(tool_idx) {
if list_get(tool_idx, k) == ix {
bind tool_json = l_set_at(tool_json, k, list_get(tool_json, k) + claude_jfield(ln, "partial_json"))
bind k = len(tool_idx)
}
bind k = k + 1.0
}
} else if str_find(ln, "message_stop") >= 0.0 {
bind done = true
} else if str_find(ln, "\"type\":\"error\"") >= 0.0 || str_find(ln, "\"type\": \"error\"") >= 0.0 {
bind emsg = "API error"
bind em = claude_jfield(ln, "message")
if em != "" { bind emsg = "API error: " + claude_unesc(em) }
st_set("cl_err", emsg)
st_set("cl_job", 0.0 - 1.0)
st_set("cl_phase", "")
st_set("cl_live", "")
st_set("ext_busy_claude", 0.0)
ide_status(emsg)
return 0.0
}
bind i = i + 1.0
}
st_set("cl_live", claude_unesc(acc))
bind code = plat_job_poll(st_get("cl_job", 0.0 - 1.0), claude_log())
if done || code > 0.0 - 1.5 {
st_set("cl_job", 0.0 - 1.0)
bind reply = claude_unesc(acc)
bind ntools = len(tool_name)
if ntools > 0.5 {
# a reply that calls tools: remember it raw, queue the calls
bind raw = "["
bind disp = reply
if acc != "" { bind raw = raw + "{\"type\":\"text\",\"text\":\"" + acc + "\"}" }
st_lclear("ct_id")
st_lclear("ct_name")
st_lclear("ct_input")
st_lclear("ct_state")
st_lclear("ct_res")
bind k = 0.0
while k < ntools {
bind input = str_trim(claude_unesc(list_get(tool_json, k)))
if input == "" { bind input = "{}" }
if k > 0.0 || acc != "" { bind raw = raw + "," }
bind raw = raw + "{\"type\":\"tool_use\",\"id\":\"" + list_get(tool_id, k) + "\",\"name\":\"" + list_get(tool_name, k) + "\",\"input\":" + input + "}"
st_lpush("ct_id", list_get(tool_id, k))
st_lpush("ct_name", list_get(tool_name, k))
st_lpush("ct_input", input)
st_lpush("ct_state", 0.0)
st_lpush("ct_res", "")
bind disp = disp + sel(disp == "", "", "\n") + "@ " + list_get(tool_name, k) + " " + claude_tool_summary(list_get(tool_name, k), input)
bind k = k + 1.0
}
bind raw = raw + "]"
st_lpush("cl_msgs", "A|" + disp + "@@RAW@@" + raw)
st_set("cl_live", "")
st_set("cl_phase", "tools")
st_set("cl_stick", 1.0)
} else if reply == "" && !done {
bind rawt = str_trim(t)
st_set("cl_err", sel(rawt == "", "No reply (exit " + num_str(code) + ") - check your network and key.", "No reply: " + g_fit(11.0, 2000.0, rawt)))
st_set("cl_phase", "")
st_set("ext_busy_claude", 0.0)
} else {
st_lpush("cl_msgs", "a|" + reply)
st_set("cl_live", "")
st_set("cl_phase", "")
st_set("ext_busy_claude", 0.0)
ide_status("Claude replied in " + fmt1(st_get("now", 0.0) - st_get("cl_t0", 0.0)) + " s")
}
}
return 0.0
}
# ── tools: queue, permission, execution ───────────────────────────────────────
fn claude_tool_summary(name, input) {
if name == "run_command" { return claude_unesc(claude_jfield(input, "command")) }
bind p = claude_unesc(claude_jfield(input, "path"))
if name == "write_file" { return p + " (" + num_str(str_len(claude_jfield(input, "content"))) + " chars)" }
return p
}
fn claude_tpath(p) { return proj_abs(claude_unesc(p)) }
fn claude_inside(p, root) {
if root == "" { return false }
return str_starts_with(str_lower(p_norm(p)), str_lower(p_norm(root)))
}
# does this call need an Allow click?
fn claude_tool_asks(k) {
bind name = st_lget("ct_name", k)
if name == "run_command" || name == "write_file" { return true }
if name == "open_file" { return false }
bind p = claude_tpath(claude_jfield(st_lget("ct_input", k), "path"))
if claude_inside(p, proj_root()) || claude_inside(p, app_root()) { return false }
return true
}
# rewrite `miao ...` to the real launcher (ling run --interp <miao>/ling/main.ling ...)
fn claude_cmd_rewrite(cmd) {
bind t = str_trim(cmd)
if t != "miao" && !str_starts_with(t, "miao ") { return cmd }
bind home = claude_miao_home()
if home == "" { return cmd }
bind rest = str_trim(str_slice(t, 4.0, str_len(t) - 4.0))
return "\"" + str_replace(ling_exe_path(), "/", "\\") + "\" run --interp \"" + str_replace(p_join(home, "ling/main.ling"), "/", "\\") + "\" " + rest + " --plain"
}
fn claude_tool_start(k) {
bind name = st_lget("ct_name", k)
bind input = st_lget("ct_input", k)
if name == "run_command" {
bind cmd = claude_cmd_rewrite(claude_unesc(claude_jfield(input, "command")))
bind dir = sel(proj_root() == "", app_root(), proj_root())
bind h = plat_job_shell(cmd, dir, p_join(cache_dir(), "claude_tool.log"))
if h < 0.0 {
st_lset("ct_res", k, "ERROR: could not start the command")
st_lset("ct_state", k, 2.0)
return 0.0
}
st_set("ct_job", h)
st_set("ct_job_t0", st_get("now", 0.0))
st_lset("ct_state", k, 1.0)
return 0.0
}
if name == "read_file" {
bind p = claude_tpath(claude_jfield(input, "path"))
if !file_exists(p) { st_lset("ct_res", k, "ERROR: not found: " + p) }
else {
bind body = plat_read(p)
if str_len(body) > 30000.0 { bind body = str_slice(body, 0.0, 30000.0) + "\n... (truncated at 30000 chars)" }
st_lset("ct_res", k, body)
}
st_lset("ct_state", k, 2.0)
return 0.0
}
if name == "list_dir" {
bind p = claude_tpath(claude_jfield(input, "path"))
if !is_true(fs_is_dir(p)) { st_lset("ct_res", k, "ERROR: not a folder: " + p) }
else {
bind items = fs_list(p)
bind out = ""
bind i = 0.0
while i < len(items) / 3.0 {
bind isdir = is_true(list_get(items, i * 3.0 + 1.0))
bind out = out + list_get(items, i * 3.0) + sel(isdir, "/", " (" + num_str(list_get(items, i * 3.0 + 2.0)) + " bytes)") + "\n"
bind i = i + 1.0
}
st_lset("ct_res", k, sel(out == "", "(empty)", out))
}
st_lset("ct_state", k, 2.0)
return 0.0
}
if name == "write_file" {
bind p = claude_tpath(claude_jfield(input, "path"))
bind content = claude_unesc(claude_jfield(input, "content"))
if plat_write(p, content) {
st_lset("ct_res", k, "wrote " + num_str(str_len(content)) + " chars to " + p)
proj_files_invalidate()
dir_invalidate(p_dir(p))
} else { st_lset("ct_res", k, "ERROR: could not write " + p) }
st_lset("ct_state", k, 2.0)
return 0.0
}
if name == "open_file" {
bind p = claude_tpath(claude_jfield(input, "path"))
bind bi = ide_open(p)
st_lset("ct_res", k, sel(bi >= 0.0, "opened in the editor", "ERROR: could not open " + p))
st_lset("ct_state", k, 2.0)
return 0.0
}
st_lset("ct_res", k, "ERROR: unknown tool " + name)
st_lset("ct_state", k, 2.0)
return 0.0
}
fn claude_tool_allow() {
bind k = st_get("ct_ask", 0.0 - 1.0)
if k >= 0.0 {
st_set("ct_ask", 0.0 - 1.0)
claude_tool_start(k)
}
return 0.0
}
fn claude_tool_deny() {
bind k = st_get("ct_ask", 0.0 - 1.0)
if k >= 0.0 {
st_set("ct_ask", 0.0 - 1.0)
st_lset("ct_res", k, "The user declined this action.")
st_lset("ct_state", k, 2.0)
}
return 0.0
}
fn claude_tools_tick() {
# a running command job?
bind n = st_llen("ct_state")
bind k = 0.0
bind active = 0.0 - 1.0
while k < n {
if st_lget("ct_state", k) == 1.0 { bind active = k }
bind k = k + 1.0
}
if active >= 0.0 {
bind code = plat_job_poll(st_get("ct_job", 0.0 - 1.0), p_join(cache_dir(), "claude_tool.log"))
if st_get("now", 0.0) - st_get("ct_job_t0", 0.0) > 120.0 {
plat_job_kill(st_get("ct_job", 0.0 - 1.0))
st_lset("ct_res", active, "ERROR: timed out after 120 s")
st_lset("ct_state", active, 2.0)
} else if code > 0.0 - 1.5 {
bind out = plat_job_log(p_join(cache_dir(), "claude_tool.log"))
if str_len(out) > 6000.0 { bind out = str_slice(out, str_len(out) - 6000.0, 6000.0) }
st_lset("ct_res", active, out + "\n[exit " + num_str(code) + "]")
st_lset("ct_state", active, 2.0)
}
return 0.0
}
# waiting on an Allow/Deny?
if st_get("ct_ask", 0.0 - 1.0) >= 0.0 { return 0.0 }
# next pending call
bind k = 0.0
while k < n {
if st_lget("ct_state", k) == 0.0 {
if claude_tool_asks(k) { st_set("ct_ask", k) }
else { claude_tool_start(k) }
return 0.0
}
bind k = k + 1.0
}
# everything done: send the results back
if n > 0.5 { claude_results_send() }
return 0.0
}
fn claude_results_send() {
bind raw = "["
bind disp = ""
bind n = st_llen("ct_id")
bind k = 0.0
while k < n {
bind res = st_lget("ct_res", k)
if k > 0.0 { bind raw = raw + "," }
bind raw = raw + "{\"type\":\"tool_result\",\"tool_use_id\":\"" + st_lget("ct_id", k) + "\",\"content\":\"" + json_escape(res) + "\"}"
bind first_line = list_get(str_split(res, "\n"), 0)
bind disp = disp + sel(k > 0.0, "\n", "") + "@ " + st_lget("ct_name", k) + " -> " + g_fit(11.0, 420.0, first_line)
bind k = k + 1.0
}
bind raw = raw + "]"
st_lpush("cl_msgs", "T|" + disp + "@@RAW@@" + raw)
st_lclear("ct_id")
st_lclear("ct_name")
st_lclear("ct_input")
st_lclear("ct_state")
st_lclear("ct_res")
st_set("ct_ask", 0.0 - 1.0)
st_set("cl_phase", "")
claude_continue()
return 0.0
}
fn claude_tools_cancel() {
bind n = st_llen("ct_state")
if n < 0.5 { return 0.0 }
bind k = 0.0
while k < n {
if st_lget("ct_state", k) == 1.0 { plat_job_kill(st_get("ct_job", 0.0 - 1.0)) }
if st_lget("ct_state", k) != 2.0 {
st_lset("ct_res", k, "Cancelled by the user.")
st_lset("ct_state", k, 2.0)
}
bind k = k + 1.0
}
st_set("ct_ask", 0.0 - 1.0)
# keep the history valid: the tool_use still gets its tool_result, but no new request
bind raw = "["
bind kk = 0.0
while kk < n {
if kk > 0.0 { bind raw = raw + "," }
bind raw = raw + "{\"type\":\"tool_result\",\"tool_use_id\":\"" + st_lget("ct_id", kk) + "\",\"content\":\"" + json_escape(st_lget("ct_res", kk)) + "\"}"
bind kk = kk + 1.0
}
bind raw = raw + "]"
st_lpush("cl_msgs", "T|@ cancelled@@RAW@@" + raw)
st_lclear("ct_id")
st_lclear("ct_name")
st_lclear("ct_input")
st_lclear("ct_state")
st_lclear("ct_res")
return 0.0
}
# ── the panel (sidebar and full-tab share this; wide mode centres a column) ───
fn claude_panel(px, py, pw, ph) {
bind x = px
bind w = pw
if pw > 720.0 {
bind w = min(860.0, pw - 48.0)
bind x = px + floor((pw - w) * 0.5)
}
bind y = py
bind h = ph
ds_word(x + 10.0, y + 10.0, 10.0, "CLAUDE", ds_col("mod"), false)
g_text_r(x + w - 34.0, y + 9.0, 10.0, C_TEXT_DIM, claude_model())
if st_get("ptab_active", 0.0) < 0.5 || st_get("ptab_stem", "") != "claude" {
if u_icon_button("cl_totab", x + w - 26.0, y + 4.0, 20.0, 18.0, "window", 10.0, C_TEXT_DIM, false) { claude_run("tab") }
if u_hot(x + w - 26.0, y + 4.0, 20.0, 18.0) { u_tip("Open the chat as a full tab") }
}
bind cy = y + 28.0
if u_chip("cl_cf", x + 8.0, cy, 44.0, 18.0, "file", st_get("cl_ctx_file", 1.0) > 0.5, ds_col("call")) { st_set("cl_ctx_file", 1.0 - st_get("cl_ctx_file", 1.0)) }
if u_chip("cl_cs", x + 56.0, cy, 42.0, 18.0, "sel", st_get("cl_ctx_sel", 0.0) > 0.5, ds_col("ctl")) { st_set("cl_ctx_sel", 1.0 - st_get("cl_ctx_sel", 0.0)) }
if u_chip("cl_cp", x + 102.0, cy, 66.0, 18.0, "problems", st_get("cl_ctx_prob", 0.0) > 0.5, ds_col("bind")) { st_set("cl_ctx_prob", 1.0 - st_get("cl_ctx_prob", 0.0)) }
if u_chip("cl_ct", x + 172.0, cy, 44.0, 18.0, "tools", claude_tools_on(), ds_col("mod")) { st_set("cl_tools", 1.0 - st_get("cl_tools", 1.0)) }
if u_hot(x + 8.0, cy, 220.0, 18.0) { u_tip("file/sel/problems attach context; tools lets Claude read, run and write with your permission") }
if u_icon_button("cl_clear", x + w - 26.0, cy - 1.0, 20.0, 20.0, "trash", 10.0, C_TEXT_DIM, false) { claude_run("clear") }
# input (a real editbox: selection, cursor, wrap, scrolling)
bind ih = sel(pw > 720.0, 92.0, 64.0)
bind by = y + h - ih - 34.0
# permission prompt sits above the input
bind ask = st_get("ct_ask", 0.0 - 1.0)
bind askh = 0.0
if ask >= 0.0 {
bind askh = 78.0
bind ay = by - askh - 4.0
bind name = st_lget("ct_name", ask)
bind summary = claude_tool_summary(name, st_lget("ct_input", ask))
px_rect(x + 6.0, ay, w - 12.0, askh, c_mix(C_BG_PANEL, ds_col("bind"), 0.08))
px_outline(x + 6.0, ay, w - 12.0, askh, ds_col("bind"), 0.9)
ds_brackets(x + 6.0, ay, w - 12.0, askh, 6.0, ds_col("bind"))
g_text(x + 16.0, ay + 7.0, 12.0, C_TEXT_STRONG, "Claude asks to " + sel(name == "run_command", "run:", sel(name == "write_file", "write:", name + ":")))
g_rect(x + 16.0, ay + 24.0, w - 32.0, 22.0, C_BG_DEEP)
ds_clip_to(x + 18.0, ay + 24.0, w - 36.0, 22.0)
g_text(x + 22.0, ay + 29.0, 11.0, E_STRING, summary)
ds_clip_end()
if u_hot(x + 16.0, ay + 24.0, w - 32.0, 22.0) { u_tip(g_fit(11.0, 900.0, summary)) }
if u_button("cl_allow", x + 16.0, ay + 50.0, 90.0, 22.0, "Allow", C_SEL) { claude_tool_allow() }
if u_button("cl_deny", x + 112.0, ay + 50.0, 90.0, 22.0, "Deny", C_RED) { claude_tool_deny() }
g_text(x + 212.0, ay + 55.0, 10.0, C_TEXT_DIM, sel(name == "run_command", "runs in " + p_base(sel(proj_root() == "", app_root(), proj_root())), ""))
}
bind sent = u_editbox("cl_in", x + 8.0, by, w - 16.0, ih, true, "Ask about your code ... (Enter sends, Shift+Enter new line)")
bind busy = claude_busy()
if busy {
if u_button("cl_stop", x + 8.0, y + h - 30.0, 76.0, 24.0, "Stop", C_RED) { claude_stop() }
bind sp = floor(st_get("now", 0.0) * 8.0) % 4.0
bind doing = sel(st_get("cl_phase", "") == "tools", "using tools", "thinking")
g_text(x + 94.0, y + h - 24.0, 11.0, ds_col("mod"), doing + str_repeat(".", sp + 1.0))
} else {
if u_button("cl_send", x + 8.0, y + h - 30.0, 76.0, 24.0, "Send", C_SEL) { bind sent = true }
g_text(x + 94.0, y + h - 24.0, 10.0, C_TEXT_DIM, "Ctrl+K asks about the selection")
}
if sent && !busy {
bind msg = str_trim(eb_text("cl_in"))
if msg != "" {
eb_clear("cl_in")
claude_send(msg)
}
}
# the conversation
bind ly = y + 52.0
bind lh = by - ly - 6.0 - sel(ask >= 0.0, askh + 4.0, 0.0)
bind total = st_get("cl_total", 100.0)
bind off = st_get("cl_scroll", 0.0)
if st_get("cl_stick", 1.0) > 0.5 { bind off = max(0.0, total - lh) }
bind noff = u_scroll_y("cl_sb", x, ly, w, lh, total, off)
if noff != off {
bind off = noff
st_set("cl_stick", sel(off >= total - lh - 8.0, 1.0, 0.0))
}
st_set("cl_scroll", off)
u_clip(x, ly, w, lh)
bind yy = ly + 4.0 - off
bind n = st_llen("cl_msgs")
if n < 0.5 && !busy && st_get("cl_err", "") == "" {
g_text_wrap(x + 12.0, ly + 8.0, w - 24.0, 11.0, C_TEXT_DIM, "Chat with Claude about the code you are editing. The chips attach the current file, the selection or the problems; with tools on, Claude can read files, run commands (miao included - never git in a temple) and write files, each run waiting for your Allow. Code blocks get Insert and Copy buttons.")
bind kh = claude_api_key()
if kh == "" {
g_text_wrap(x + 12.0, ly + 100.0, w - 24.0, 11.0, ds_col("bind"), "First: set your Anthropic API key in Settings (Ctrl+,), or set ANTHROPIC_API_KEY before starting LingCoder.")
if u_button("cl_setkey", x + 12.0, ly + 144.0, 130.0, 24.0, "Open Settings", C_SEL) { ide_run_action("file.settings") }
}
}
bind i = 0.0
while i < n {
bind m = st_lget("cl_msgs", i)
bind kind = 1.0
bind text = ""
if str_starts_with(m, "u|") {
bind kind = 0.0
bind text = str_slice(m, 2.0, str_len(m) - 2.0)
} else if str_starts_with(m, "a|") {
bind text = str_slice(m, 2.0, str_len(m) - 2.0)
} else if str_starts_with(m, "A|") {
bind text = claude_display_of(m)
} else {
bind kind = 2.0
bind text = claude_display_of(m)
}
bind yy = claude_msg_draw(x, yy, w, text, kind, ly, ly + lh, i)
bind i = i + 1.0
}
if busy && st_get("cl_live", "") != "" {
bind yy = claude_msg_draw(x, yy, w, st_get("cl_live", ""), 1.0, ly, ly + lh, 999.0)
}
if st_get("cl_err", "") != "" {
g_text(x + 12.0, yy + 4.0, 10.0, ds_col("asm"), "ERROR")
bind yy = yy + 18.0 + g_text_wrap(x + 12.0, yy + 18.0, w - 24.0, 11.0, ds_col("asm"), st_get("cl_err", "")) + 6.0
}
u_unclip()
st_set("cl_total", yy + off - ly + 12.0)
return 0.0
}
# one message: role tag + copy, wrapped prose, boxed code with Insert / Copy.
# kind: 0 = you, 1 = claude, 2 = tools. returns the y below it
fn claude_msg_draw(x, yy, w, text, kind, clip0, clip1, idx) {
bind col = ds_col("call")
bind tag = "YOU"
if kind == 1.0 { bind col = ds_col("mod") bind tag = "CLAUDE" }
if kind == 2.0 { bind col = ds_col("ctl") bind tag = "TOOLS" }
if yy + 14.0 > clip0 && yy < clip1 {
ds_word(x + 10.0, yy, 9.0, tag, col, false)
bind cpx = x + 16.0 + ds_word_w(9.0, tag)
if u_hot(cpx, yy - 1.0, 16.0, 13.0) {
u_tip("Copy this message")
if u_pressed() {
plat_clip_set(text)
ide_status("Message copied")
}
}
icon_draw("copy", cpx + 8.0, yy + 5.0, 8.0, sel(u_hot(cpx, yy - 1.0, 16.0, 13.0), C_TEXT_STRONG, C_LINE))
}
bind yy = yy + 15.0
bind parts = str_split(text, "```")
bind pi = 0.0
while pi < len(parts) {
bind seg = list_get(parts, pi)
if pi % 2.0 < 0.5 {
bind paras = str_split(seg, "\n")
bind k = 0.0
while k < len(paras) {
bind p = str_trim(list_get(paras, k))
if p != "" {
bind pc = sel(kind == 0.0, C_TEXT, C_TEXT_STRONG)
if str_starts_with(p, "@ ") { bind pc = ds_col("ctl") }
bind hh = g_text_wrap(x + 12.0, yy, w - 26.0, 11.0, pc, p)
bind yy = yy + hh + 3.0
}
bind k = k + 1.0
}
} else {
bind code = seg
bind nl = str_find(code, "\n")
if nl >= 0.0 && nl < 12.0 { bind code = str_slice(code, nl + 1.0, str_len(code) - nl - 1.0) }
bind clines = str_split(code, "\n")
while len(clines) > 0.5 && str_trim(list_get(clines, len(clines) - 1.0)) == "" { bind clines = l_remove_at(clines, len(clines) - 1.0) }
bind ch = len(clines) * 15.0 + 12.0
if yy + ch + 22.0 > clip0 && yy < clip1 {
g_rect(x + 10.0, yy, w - 22.0, ch, E_BG)
px_outline(x + 10.0, yy, w - 22.0, ch, c_mix(C_BG_PANEL, col, 0.5), 0.7)
ds_brackets(x + 10.0, yy, w - 22.0, ch, 5.0, col)
ds_clip_to(x + 12.0, yy + 2.0, w - 26.0, ch - 4.0)
bind k = 0.0
while k < len(clines) {
g_text(x + 16.0, yy + 6.0 + k * 15.0, 11.0, E_TEXT, list_get(clines, k))
bind k = k + 1.0
}
ds_clip_end()
}
bind yy = yy + ch + 2.0
bind cid = num_str(idx) + "_" + num_str(pi)
if yy + 20.0 > clip0 && yy < clip1 {
if u_button("cl_ins" + cid, x + 10.0, yy, 58.0, 18.0, "Insert", C_SEL) {
ide_insert(claude_block_text(clines))
ide_status("Inserted at the cursor")
}
if u_button("cl_cp" + cid, x + 72.0, yy, 52.0, 18.0, "Copy", 0.0) {
plat_clip_set(claude_block_text(clines))
ide_status("Code copied")
}
}
bind yy = yy + 24.0
}
bind pi = pi + 1.0
}
return yy + 8.0
}
fn claude_block_text(clines) {
bind s = ""
bind k = 0.0
while k < len(clines) {
bind s = s + list_get(clines, k) + sel(k < len(clines) - 1.0, "\n", "")
bind k = k + 1.0
}
return s
}