灵符lingfu
L

lingcoder

↓ 0 downloads

LingCoder: 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 lingcoder
LicenseApache-2.0 OR MIT
Authorsling-lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / 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
}

← Back to index