diff --git a/.claude/control/SKILL.md b/.claude/control/SKILL.md new file mode 100644 index 0000000000..a2ff6f66ff --- /dev/null +++ b/.claude/control/SKILL.md @@ -0,0 +1,168 @@ +--- +name: control +description: "Control the Geany editor UI from the agent terminal — open files, save, scroll to changes, refresh the file tree" +argument-hint: " [args] | save-all | refresh | open | scroll : | list" +allowed-tools: + - Bash + - Read +--- + + +Drive the live Geany editor from within this agent session using the geanycontrol plugin. + +This skill lets the agent: +- Save open files before starting work so Geany does not prompt about conflicts +- Open files the agent just created or modified +- Scroll Geany to the exact line of a change +- Refresh the treebrowser after adding or deleting files on disk +- Query which files are currently open +- Trigger Tools-menu items by name + +All operations go through the geany-ctrl script which talks to the +geanycontrol Unix socket at ~/.config/geany/geanycontrol.sock. +The plugin must be loaded in the running Geany instance. + + + +## Socket path + + ~/.config/geany/geanycontrol.sock + (expands to: $XDG_CONFIG_HOME/geany/geanycontrol.sock if XDG_CONFIG_HOME is set) + +## geany-ctrl script location (in this repo) + + /home/teknopaul/github_workspace/geany-plugins/geanycontrol/geany-ctrl + +Add it to PATH or call it with the full path. Requires `socat` at runtime. + +## Availability check + +Before sending commands, verify the plugin is running: + + printf 'ping\n' | socat - UNIX-CONNECT:$HOME/.config/geany/geanycontrol.sock + +If the socket does not exist, Geany is not running or the GeanyControl +plugin is not loaded — skip UI operations, they are not required for the +work to succeed. + +## All commands + +| Command | What it does | +|---------|-------------| +| `ping` | Liveness check; always replies `ok` | +| `save-all` | Save every unsaved open document | +| `save-file ` | Save one specific document | +| `open-file ` | Open a file in the editor | +| `close-file ` | Close a document (unsaved ch1nges are discarded) | +| `scroll-to-line :` | Open file and jump to line (1-based) | +| `get-current-file` | Return the path of the currently active document | +| `list-open-files` | Return newline-separated list of all open file paths | +| `activate-menu-item + + +Determine what the user (or prior task) needs done with the Geany UI, then +execute the minimal set of geany-ctrl commands to accomplish it. + +## Step 1 — Check availability + + SOCK="$HOME/.config/geany/geanycontrol.sock" + if [ ! -S "$SOCK" ]; then + # Geany not running or plugin not loaded — skip UI steps, note in output + exit 0 + fi + +## Step 2 — Common workflow patterns + +### Before starting work on files Geany has open + + geany-ctrl save-all + +This prevents "file changed on disk" prompts while the agent edits files. + +### After editing or creating files + + geany-ctrl refresh + +This reloads the treebrowser so newly created or deleted files appear +immediately. + +### Open a specific file + + geany-ctrl open-file /path/to/changed-file.c + +### Jump to a specific line (e.g., where a bug was fixed) + + geany-ctrl scroll-to-line /path/to/changed-file.c:42 + +### Full post-edit sequence + + geany-ctrl refresh + geany-ctrl open-file /path/to/changed-file.c + geany-ctrl scroll-to-line /path/to/changed-file.c:42 + +### Query what is open before deciding what to close + + geany-ctrl list-open-files + +## Step 3 — Report result + +If any command returns a line starting with `error:`, report it to the user. +`ok` responses can be silently swallowed unless the user asked for verbose output. + +## Error handling + +- Socket missing → note "GeanyControl not available" and continue without UI steps. +- `error: could not open file` → the path does not exist; verify the path before retrying. +- `error: menu item not found` → the label did not match; check the exact label text + in Geany's Tools menu. +- `socat` not installed → install it (`sudo apt install socat`) or use the raw + printf/socat one-liner pattern from the context section above. + diff --git a/.claude/skills/c-bash/SKILL.md b/.claude/skills/c-bash/SKILL.md new file mode 100644 index 0000000000..90cd9128d8 --- /dev/null +++ b/.claude/skills/c-bash/SKILL.md @@ -0,0 +1,54 @@ +--- +name: c-bash +description: make .sh files follow common C code conventions for readability and maintainability +allowed-tools: bash +--- + +All `.sh` files should follow `.claude/skills/bash-coding-standards.md` + +You MUST work on one file at a time, don't do bulk updates since these tend to break scripts subtly. + +When replacing `"${var}"` it is _not_ always safe to use `$var`, especially if the variable might contain spaces or unknown input. + +It is only safe to use the shortened `$var` syntax when the variable is known to be a simple string value, usually that means its set earlier in the script. + +It is also only safe to remove the `{}` if the use of this variable does not cause ambiguity, or if the text following it does not cause bash to interpret it as a different variable. + +After making changes to each file one at a time you must `bash -n` the changed script and then run the script to make sure it still works. + +## converting non-compliant code + +When asked to convert bash to to these conventions... + +Never do mass sed/perl replacements across many files at once. + +Each change must be read in context first. A variable that looks like a path may hold multi-line output, quoted strings, or JSON. The fix that is correct for one variable may silently corrupt another. + +Quotes are load-bearing — check usage before removing them +"$var" is NOT just style. It is required when: +- the variable may contain spaces (file names, URLs with query strings, response bodies) +- the variable may contain newlines (curl response bodies, grep output) +- the variable may contain glob characters (*, ?, [) +- the variable is on the right-hand side of [[ ]] and could contain * or ? + +- Only remove quotes when you can see, from reading the script, that none of these apply. +echo "$var" must stay quoted when var is a response body +echo $var word-splits and glob-expands. echo "$var" preserves newlines. Any variable holding HTTP response bodies, JSON, or log output must keep quotes. + +Work iteratively — one file at a time + +1. Read the full file +1. Identify violations +1. Understand each variable's content and all its usages +1. Make targeted changes +1. Run bash -n and the test before moving to the next file + +Never use automated tools (sed/perl/python) to rename variables across files + +Variable names are not unique tokens — $body in one script is a response body requiring quotes; in another it is a simple path. Mass rename without reading context will introduce bugs. +The rule is: remove ${braces} only, not quotes + +"${foo}" → "$foo" is usually safe (keeps quotes, removes unnecessary braces). +If any code uses "${foo}_baa" changing to "$foo_bar" is not safe. + +"${foo}" → $foo is only safe after reading the specific usage. diff --git a/.claude/skills/c-bash/bash-coding-standards.md b/.claude/skills/c-bash/bash-coding-standards.md new file mode 100644 index 0000000000..afcfde6695 --- /dev/null +++ b/.claude/skills/c-bash/bash-coding-standards.md @@ -0,0 +1,43 @@ +# Bash coding standards + +All test `.sh` scripts should follow these guidelines. + +follow [bash code style](bash-code-style.md). + + +Presume no spaces in paths, this is Linux, no need to quote them. +Presume URLs do not have spaces, they never do, no need to quote them. +Don't quote `$port` variables +Don't use colors `${RED}` outside of the test utilities. +Avoid env vars for nodejs server parameters, use CLI args. + +Make use of test utilities `test-functions.sh` and `integration-test-fucntion.sh`, do not write script specific + +- assertions +- kill functions + +Finish scripts with `test_summary` this will exit in a way that integrates with Makefile test targets. +For nginx integration tests just before `test_summary` run `crash_check` + +## exit handlers + +Handle cleanup in an exit handler, that runs in success or failure scenario. +``` +cleanup() { + cleanup_nginx + cleanup_pid $backend_pid + cleanup_temp_dir +} +trap cleanup EXIT +``` + +# Prefer standard ports + +mxsrv 53880 +legacy 8080 +winchecker-next 9090 +cache_manager 9999 + +# preferred vars + +base_url diff --git a/.claude/skills/geany-progress/SKILL.md b/.claude/skills/geany-progress/SKILL.md new file mode 100644 index 0000000000..36256891b6 --- /dev/null +++ b/.claude/skills/geany-progress/SKILL.md @@ -0,0 +1,194 @@ +--- +name: geany-progress +description: "Report AI agent plan progress to the Geany sidebar via the geanyprogress plugin socket API" +argument-hint: "[init | done N | status | check]" +allowed-tools: + - Bash +--- + +# geany-progress skill + +Report plan progress to the **geanyprogress** Geany plugin. The plugin renders a live +progress panel in the Geany sidebar and persists state to `.planning/state/xxxx_PROGRESS.md`. + +## File naming conventions + +| Artifact | Path | +|----------|------| +| Plan | `.planning/plans/MY_PLAN_PLAN.md` | +| Progress | `.planning/state/MY_PLAN_PROGRESS.md` | + +The slug prefix is UPPERCASE_SNAKE_CASE derived from the plan name +(e.g. plan name `"My Task"` → slug `MY_TASK` → files `MY_TASK_PLAN.md` / `MY_TASK_PROGRESS.md`). + +## How the API works + +The plugin listens on a Unix domain socket. Its path is exported as `GEANY_PROGRESS_SOCK` +in the Geany process environment — every terminal opened inside Geany (geanycli, +geanyagent, VTE) inherits this variable automatically. + +Two JSON message types: + +``` +# Register (or replace) a plan — plan_file is optional but enables "Open plan" +{"plan":"Plan Name","plan_file":".planning/plans/PLAN_NAME_PLAN.md","phases":["Phase 1","Phase 2","Phase 3"]} + +# Mark a phase complete (1-based index), with optional review files and warnings +{"phase":1,"status":"complete", + "files":[{"path":"src/foo.c","line":42},{"path":"src/bar.h"}], + "warnings":["Possible memory leak on error path","Check timeout handling"]} +``` + +`files` and `warnings` are optional. When present: +- **files** — clicking the completed phase row in the sidebar opens each file at the given line +- **warnings** — shown as a tooltip when hovering over the phase row + +Messages are sent by writing to the socket with `nc -U` or `socat`. + +## Helper script + +`geany-progress` (on `$PATH` when the plugin directory is in `PATH`, or at +`geany-plugins/geanyprogress/geany-progress`) wraps the raw socket calls: + +```sh +geany-progress init [-f plan_file] "Plan Name" "Phase 1" "Phase 2" "Phase 3" +geany-progress done N [-r file[:line]]... [-w "warning"]... +geany-progress status +``` + +Flags for `done`: +- `-r path` or `-r path:line` — mark a file (and optional line) for review; repeat for multiple files +- `-w "message"` — add a warning shown on hover; repeat for multiple warnings + +## Sidebar interactions + +| Interaction | Action | +|-------------|--------| +| Left-click title row | Open `_PLAN.md` in Geany editor | +| Left-click completed phase | Open all review files at their line numbers | +| Hover over any phase | Show warnings tooltip (⚠ messages + file list) | +| Right-click → Mark finished | Mark the clicked phase complete | +| Right-click → Open review files | Open review files for the right-clicked phase | +| Right-click → Open progress | Open `_PROGRESS.md` in editor | +| Right-click → Open plan | Open `_PLAN.md` in editor | +| Right-click → Load... | File chooser: load any `_PROGRESS.md` into the GUI | + +Done phases show a green ✓; pending phases show ○. +Phases with warnings show them as a tooltip on hover. + +## Usage patterns + +### Check whether the plugin is available + +```sh +if [ -n "$GEANY_PROGRESS_SOCK" ] && [ -S "$GEANY_PROGRESS_SOCK" ]; then + echo "geanyprogress available" +fi +``` + +### Register a plan at the start of a task + +```sh +# With plan file reference (recommended — enables "Open plan" in sidebar) +geany-progress init -f ".planning/plans/MY_TASK_PLAN.md" \ + "My Task" "Research" "Implement" "Test" "Ship" + +# Without plan file (sidebar title click does nothing) +geany-progress init "My Task" "Research" "Implement" "Test" "Ship" +``` + +Or without the helper (plain shell): + +```sh +printf '{"plan":"My Task","plan_file":".planning/plans/MY_TASK_PLAN.md","phases":["Research","Implement","Test","Ship"]}' \ + | nc -U "$GEANY_PROGRESS_SOCK" + +# Mark phase 2 done with review file and warning +printf '{"phase":2,"status":"complete","files":[{"path":"src/engine.c","line":117}],"warnings":["Check thread safety"]}' \ + | nc -U "$GEANY_PROGRESS_SOCK" +``` + +### Mark a phase done + +```sh +geany-progress done 2 # simple completion +geany-progress done 2 -r src/foo.c:42 # with a review file at a specific line +geany-progress done 2 -r src/foo.c:42 \ + -r src/bar.h \ + -w "Check error handling on line 42" \ + -w "Possible memory leak in cleanup path" +``` + +**When to add review files and warnings:** + +- Add `-r` for files the human should manually inspect — typically non-trivial C/C++ changes, security-sensitive code, or complex logic. Skip generated files, Makefiles, and boilerplate scripts. +- Add `-w` for anything the human should know before approving: potential regressions, assumptions that could be wrong, deferred TODOs, or anything that needed a trade-off decision. +- Keep it focused — 1-3 files and 1-2 warnings is ideal. The point is to surface the *interesting* changes, not enumerate every modified file. + +### Wrap a multi-phase agent workflow + +```sh +# At the start — pairs progress with the plan document +geany-progress init -f ".planning/plans/CODE_REVIEW_PLAN.md" \ + "Code Review" "Read files" "Analyse" "Write findings" + +# ... read the files ... +geany-progress done 1 + +# ... analyse ... +geany-progress done 2 + +# ... write output — flag the interesting output file ... +geany-progress done 3 -r findings.md:1 +``` + +## Execution + +When this skill is invoked, execute the sub-command passed as `$ARGUMENTS`: + +### `check` or no argument + +Verify the socket is reachable and report its path: + +```sh +if [ -z "$GEANY_PROGRESS_SOCK" ]; then + echo "GEANY_PROGRESS_SOCK is not set — geanyprogress plugin may not be loaded" +elif [ ! -S "$GEANY_PROGRESS_SOCK" ]; then + echo "Socket path set to $GEANY_PROGRESS_SOCK but file does not exist" +else + echo "geanyprogress OK — socket at $GEANY_PROGRESS_SOCK" + ls -la "$GEANY_PROGRESS_SOCK" +fi +``` + +### `init [-f plan_file] [phases...]` + +Register a new plan. Extract arguments from `$ARGUMENTS` and call `geany-progress init`. +Always pass `-f` when the plan file is known. + +Example: `/geany-progress init -f ".planning/plans/DEPLOY_PLAN.md" "Deploy" "Build" "Test" "Push"` + +### `done N [-r file[:line]]... [-w "warning"]...` + +Mark phase N complete, optionally with review files and warnings. Pass all arguments +from `$ARGUMENTS` directly to `geany-progress done`. + +Examples: +- `/geany-progress done 2` +- `/geany-progress done 3 -r src/engine.c:117 -w "Review error handling"` + +### `status` + +Run `geany-progress status` to show the socket path and confirm the socket file exists. + +## Notes for agents without access to the source + +- You do not need the plugin source — just `nc` (or `socat`) and `$GEANY_PROGRESS_SOCK`. +- `GEANY_PROGRESS_SOCK` is set automatically when Geany loads the plugin; set it manually + only when running outside a Geany terminal (e.g. a CI script pointing at a running Geany). +- Phase indices are **1-based**. +- Sending a second `init` message replaces the active plan entirely. +- Progress is also persisted to `.planning/state/XXX_PROGRESS.md` in the open project + root after every update — readable from the filesystem without touching the socket. +- The socket accepts one message per connection; `nc -U` handles this correctly by default. +- Relative `plan_file` paths are resolved against the Geany project root automatically. diff --git a/.claude/skills/mdalign/SKILL.md b/.claude/skills/mdalign/SKILL.md new file mode 100644 index 0000000000..b2222dbce5 --- /dev/null +++ b/.claude/skills/mdalign/SKILL.md @@ -0,0 +1,25 @@ +--- +name: mdalign +description: Align tables in markdown files +--- + +user will indicate which file needs tables adjusted, if skill is run with no arguments align the last created md file + + +Ensure the text representation of tables, is easy to read wihtout rendering the markdown as HTML + + + +Adjust the whitespace and `|----|` header markers or all tables in the file to fixed width of the longest text in a cell in the tables column. + +N.B. headers should have no whitespace characters + +e.g + +``` +| # | Task | Classes | Est. Lines | Est. Gain | +|-----|------------------------------------------------------------------|--------------------|------------|-------------------| +``` + +Use the .claude/skills/mdalign/mdalign.py script to achieve this, then check it worked + diff --git a/.claude/skills/mdalign/mdalign.py b/.claude/skills/mdalign/mdalign.py new file mode 100755 index 0000000000..927078ddc3 --- /dev/null +++ b/.claude/skills/mdalign/mdalign.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Align all markdown tables in a file to fixed column widths.""" + +import re +import sys + + +def parse_row(line): + cells = line.strip().split('|') + return [c.strip() for c in cells[1:-1]] + + +def is_separator(row): + return all(re.fullmatch(r':?-+:?', c) for c in row if c) + + +def align_table(lines): + rows = [parse_row(l) for l in lines] + ncols = max(len(r) for r in rows) + + # Pad all rows to same column count + for r in rows: + while len(r) < ncols: + r.append('') + + sep_idx = next((i for i, r in enumerate(rows) if is_separator(r)), None) + + # Column widths based on non-separator rows only + widths = [ + max(len(r[c]) for i, r in enumerate(rows) if i != sep_idx) + for c in range(ncols) + ] + + result = [] + for i, row in enumerate(rows): + if i == sep_idx: + result.append('|' + '|'.join('-' * (w + 2) for w in widths) + '|') + else: + result.append('|' + '|'.join(f' {row[c].ljust(widths[c])} ' for c in range(ncols)) + '|') + return result + + +def align_file(path): + with open(path) as f: + lines = f.readlines() + + output = [] + i = 0 + while i < len(lines): + line = lines[i].rstrip('\n') + if re.match(r'\s*\|.*\|', line): + block = [] + while i < len(lines) and re.match(r'\s*\|.*\|', lines[i].rstrip('\n')): + block.append(lines[i].rstrip('\n')) + i += 1 + output.extend(align_table(block)) + else: + output.append(line) + i += 1 + + with open(path, 'w') as f: + f.write('\n'.join(output) + '\n') + print(f"Aligned: {path}") + + +if __name__ == '__main__': + if len(sys.argv) < 2: + print("Usage: mdalign.py [file2.md ...]") + sys.exit(1) + for path in sys.argv[1:]: + align_file(path) diff --git a/.claude/skills/nowrite/SKILL.md b/.claude/skills/nowrite/SKILL.md new file mode 100644 index 0000000000..11525a5f79 --- /dev/null +++ b/.claude/skills/nowrite/SKILL.md @@ -0,0 +1,9 @@ +--- +name: nowrite +description: ensure agents dont change files when background work is ongoing +allowed-tools: bash, read_file, sed, grep, find +--- + +This skill simply states that no changes should be made to the file system during the operation of the agent's task. +Agent is free to read any files. Human is working on the code at the same time and may be changing files or running tests. +Agen may write docs and plans in ai-output/. \ No newline at end of file diff --git a/.claude/skills/opus-exec/SKILL.md b/.claude/skills/opus-exec/SKILL.md new file mode 100644 index 0000000000..97bb8abdcd --- /dev/null +++ b/.claude/skills/opus-exec/SKILL.md @@ -0,0 +1,44 @@ +--- +name: opus-exec +description: executing a plan written by opus-plan +allowed-tools: java, bash +--- + +Claude Opus writes phased delivery plans in `.planning/plans/xxxx_PLAN.md`. + +Claude Sonnet should execute. If the agent asked to execute is Opus it should stop +and report this as an error. + +Arguments to this skill should be the plan file path and optionally the starting phase. +The plan may also be provided as an attached document to the chat. + +## File naming convention + +- Plans: `.planning/plans/xxxx_PLAN.md` +- Progress: `.planning/state/xxxx_PROGRESS.md` (written by geanyprogress automatically) + +## Start of execution + +The plan was registered with the Geany sidebar by `opus-plan` when it was written. +Do not re-run `geany-progress init` — that would replace the active plan. + +## During execution + +Execute phases autonomously, one after the other, until the context window is 80% full. + +After completing each phase, run the `geany-progress done` command from the plan, which +includes review files and warnings specific to that phase: + +```sh +geany-progress done N [-r path[:line]]... [-w "warning"]... +``` + +The plugin writes `.planning/state/xxxx_PROGRESS.md` automatically after each call. + +If the context window reaches 80%, report the last completed phase number and wait for +the human to `/clear` and continue with `/opus-exec` from the next phase. + +## Constraints + +Never submit code. +Never change `.perf.targets` — humans must review performance regressions. diff --git a/.claude/skills/opus-plan/SKILL.md b/.claude/skills/opus-plan/SKILL.md new file mode 100644 index 0000000000..1d9621ebc9 --- /dev/null +++ b/.claude/skills/opus-plan/SKILL.md @@ -0,0 +1,42 @@ +--- +name: opus-plan +description: use Claude Opus to plan development for Claude Sonnet +allowed-tools: java, bash +--- + +Rather than implementing any code, output should be a markdown file in +`.planning/plans/xxxx_PLAN.md` that contains a phased implementation plan where each +phase fits in the Claude Sonnet 4.6 context window. + +The filename slug `xxxx` should be a short kebab-case description of the plan +(e.g. `geany-progress_PLAN.md`, `auth-refactor_PLAN.md`). + +No changes should be made other than writing the one new plan document and registering +the plan with the Geany progress sidebar. + +## Register the plan with the sidebar + +After writing the plan file, run `geany-progress init` to register it: + +```sh +geany-progress init -f ".planning/plans/xxxx_PLAN.md" "Plan Name" \ + "Phase 1 title" "Phase 2 title" ... +``` + +This pairs the sidebar entry with the plan file so clicking the title opens it in Geany. +If `$GEANY_PROGRESS_SOCK` is not set or the socket does not exist, skip silently. + +## Phase completion markers + +At the end of each phase section, include the exact `geany-progress done` command that +Claude Sonnet should run after completing that phase: + +```sh +geany-progress done N [-r path[:line]]... [-w "warning"]... +``` + +where N is the 1-based phase number. Include `-r` flags for the key files the human +should review and `-w` flags for any important caveats or trade-offs. + +Progress state is tracked in `.planning/state/xxxx_PROGRESS.md` (written automatically +by the geanyprogress plugin after each `geany-progress done` call). diff --git a/.planning/state/api-test.md b/.planning/state/api-test.md new file mode 100644 index 0000000000..51d2e567b5 --- /dev/null +++ b/.planning/state/api-test.md @@ -0,0 +1,7 @@ +# API Test — Progress + +| Phase | Description | Status | +|-------|-------------|--------| +| 1 | Phase A | complete | +| 2 | Phase B | complete | +| 3 | Phase C | pending | diff --git a/.planning/state/geanyprogress-build.md b/.planning/state/geanyprogress-build.md new file mode 100644 index 0000000000..d38314aa59 --- /dev/null +++ b/.planning/state/geanyprogress-build.md @@ -0,0 +1,10 @@ +# geanyprogress Build — Progress + +| Phase | Description | Status | +|-------|-------------|--------| +| 1 | Scaffold | complete | +| 2 | Socket | complete | +| 3 | JSON Parser | complete | +| 4 | GTK UI | complete | +| 5 | Persistence | complete | +| 6 | Helper Script | complete | diff --git a/.planning/state/skill-verification.md b/.planning/state/skill-verification.md new file mode 100644 index 0000000000..e63ade0698 --- /dev/null +++ b/.planning/state/skill-verification.md @@ -0,0 +1,8 @@ +# Skill Verification — Progress + +| Phase | Description | Status | +|-------|-------------|--------| +| 1 | Check socket | complete | +| 2 | Send init | complete | +| 3 | Mark done | complete | +| 4 | Verify persistence | complete | diff --git a/AIddina.png b/AIddina.png new file mode 100644 index 0000000000..7d236f7558 Binary files /dev/null and b/AIddina.png differ diff --git a/MAINTAINERS b/MAINTAINERS index 8a7a8efdea..817003f28a 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -311,3 +311,31 @@ g: @earshinov M: Eugene Arshinov W: http://plugins.geany.org/xmlsnippets.html S: Maintained + +geanyagent +P: teknopaul +G: @teknopaul +M: teknopaul +W: https://github.com/teknopaul/geany-plugins +S: Maintained + +geanycli +P: teknopaul +G: @teknopaul +M: teknopaul +W: https://github.com/teknopaul/geany-plugins +S: Maintained + +geanyservers +P: teknopaul +G: @teknopaul +M: teknopaul +W: https://github.com/teknopaul/geany-plugins +S: Maintained + +locate +P: teknopaul +G: @teknopaul +M: teknopaul +W: https://github.com/teknopaul/geany-plugins +S: Maintained diff --git a/Makefile.am b/Makefile.am index a3f1c10f80..9d6581f37d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -95,6 +95,38 @@ if ENABLE_GENIUSPASTE SUBDIRS += geniuspaste endif +if ENABLE_GEANYAGENT +SUBDIRS += geanyagent +endif + +if ENABLE_GEANYENV +SUBDIRS += geanyenv +endif + +if ENABLE_GEANYPROGRESS +SUBDIRS += geanyprogress +endif + +if ENABLE_LINUXRECTSELECT +SUBDIRS += linuxrectselect +endif + +if ENABLE_GEANYCLI +SUBDIRS += geanycli +endif + +if ENABLE_GEANYCONTROL +SUBDIRS += geanycontrol +endif + +if ENABLE_GEANYVOSK +SUBDIRS += geanyvosk +endif + +if ENABLE_GEANYSERVERS +SUBDIRS += geanyservers +endif + if ENABLE_GITCHANGEBAR SUBDIRS += git-changebar endif @@ -111,6 +143,10 @@ if ENABLE_LIPSUM SUBDIRS += lipsum endif +if ENABLE_LOCATE +SUBDIRS += locate +endif + if ENABLE_LSP SUBDIRS += lsp endif @@ -159,6 +195,10 @@ if ENABLE_TABLECONVERT SUBDIRS += tableconvert endif +if ENABLE_JACOCOCOVERAGE +SUBDIRS += jacococoverage +endif + if ENABLE_TREEBROWSER SUBDIRS += treebrowser endif diff --git a/ai-context/PROGRESS.md b/ai-context/PROGRESS.md new file mode 100644 index 0000000000..54b04e7255 --- /dev/null +++ b/ai-context/PROGRESS.md @@ -0,0 +1,43 @@ +# geanyprogress — AI Agent Plan Progress Panel + +## Status + +| Phase | Description | Status | +|-------|------------------------------------------|---------| +| 1 | Plugin scaffold & build system | complete | +| 2 | Unix domain socket server | complete | +| 3 | JSON parsing + in-memory model | complete | +| 4 | GTK panel UI | complete | +| 5 | Persistence (.planning/state/) | complete | +| 6 | geanyenv integration + helper script | complete | + +## Notes + +- Plan document: `ai-context/geanyprogress-plan.md` +- Architecture: Unix domain socket at `/tmp/geany-progress-.sock`, env var `GEANY_PROGRESS_SOCK` +- To resume after `/clear`: + "Continue geanyprogress Phase N per ai-context/geanyprogress-plan.md. Current progress is in ai-context/PROGRESS.md." + +--- + +# geanyvosk — Implementation Progress + +## Status + +| Phase | Description | Status | +|-------|--------------------------------------|----------| +| 1 | Project scaffold & build system | complete | +| 2 | ALSA microphone capture thread | complete | +| 3 | Vosk ASR + wake word detection | complete | +| 4 | UI command dispatch via geanycontrol | complete | +| 5 | Skill & AI-prompt creation by voice | complete | +| 6 | Agent skill execution by voice | complete | + +## Notes + +- Plan document: `ai-context/geanyvosk-plan.md` +- Vosk is NOT installed on the system; must be installed before Phase 3. + - Install libvosk: download from https://alphacephei.com/vosk/models (C library + header) + - Install ALSA dev: `sudo apt install libasound2-dev` +- To resume after `/clear`, start a new session and say: + "Continue geanyvosk Phase N per ai-context/geanyvosk-plan.md" diff --git a/ai-context/geanyprogress-plan.md b/ai-context/geanyprogress-plan.md new file mode 100644 index 0000000000..765b55ba5c --- /dev/null +++ b/ai-context/geanyprogress-plan.md @@ -0,0 +1,774 @@ +# geanyprogress — AI Agent Plan Progress Panel + +## Implementation Plan + +**Spec source:** `ai-prompts/progress-pannel.prompt.md` +**Model:** Claude Sonnet 4.6 (one phase per context window) +**Progress tracking:** `./ai-context/PROGRESS.md` +**Architecture decision:** Option B — Unix domain socket + env var (see conversation context) + +--- + +## Architecture Overview + +A new Geany plugin `geanyprogress` (directory: `geany-plugins/geanyprogress/`) written in C. + +It opens a **Unix domain socket** at `/tmp/geany-progress-.sock` and sets +`GEANY_PROGRESS_SOCK` in the Geany process environment so all child terminals (geanycli, +geanyagent) inherit the path. + +An AI agent running inside a VTE terminal writes JSON to the socket to register a plan +and mark phases complete. The plugin renders the plan in a **sidebar panel** (left pane, +beside the treebrowser) using a `GtkTreeView`. + +After every update the plugin also writes `.planning/state/.md` in the project +root for version-control persistence. + +### Socket API (two message types) + +``` +# Register/replace the active plan +{"plan":"Plan Name","phases":["Phase 1","Phase 2","Phase 3"]} + +# Mark a phase complete (1-based index) +{"phase":1,"status":"complete"} +``` + +### Agent helper script + +```sh +geany-progress init "Plan Name" "Phase 1" "Phase 2" "Phase 3" +geany-progress done 1 +geany-progress status +``` + +--- + +## Build system conventions (from existing plugins) + +Every plugin follows the same skeleton: + +``` +geanyprogress/ + Makefile.am # SUBDIRS = src; plugin = geanyprogress + src/ + Makefile.am # geanyplugins_LTLIBRARIES = geanyprogress.la + geanyprogress.c +``` + +`geany-plugins/Makefile.am` gains: +```makefile +if ENABLE_GEANYPROGRESS +SUBDIRS += geanyprogress +endif +``` + +`configure.ac` gains `GP_CHECK_PLUGIN(geanyprogress)` (copy pattern from geanyenv entry). + +--- + +## Phase 1 — Plugin Scaffold & Build System + +**Goal:** Bare plugin compiles and loads into Geany. Sidebar tab is visible with static +placeholder text. No socket yet. + +### Files to create + +**`geanyprogress/Makefile.am`** +```makefile +include $(top_srcdir)/build/vars.auxfiles.mk + +AUXFILES = + +SUBDIRS = src +plugin = geanyprogress +``` + +**`geanyprogress/src/Makefile.am`** +```makefile +include $(top_srcdir)/build/vars.build.mk +plugin = geanyprogress + +geanyplugins_LTLIBRARIES = geanyprogress.la + +geanyprogress_la_SOURCES = geanyprogress.c +geanyprogress_la_CPPFLAGS = $(AM_CPPFLAGS) -I$(srcdir) +geanyprogress_la_CFLAGS = $(AM_CFLAGS) $(GIO_CFLAGS) +geanyprogress_la_LIBADD = $(COMMONLIBS) $(GIO_LIBS) + +include $(top_srcdir)/build/cppcheck.mk +``` + +Note: `$(GIO_CFLAGS)` / `$(GIO_LIBS)` are already available from the top-level +`configure.ac` because treebrowser uses GIO — confirm with `grep GIO configure.ac`. +If not present, add `PKG_CHECK_MODULES([GIO], [gio-2.0])` to configure.ac. + +**`geanyprogress/src/geanyprogress.c`** — minimal skeleton: +```c +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include +#include + +GeanyPlugin *geany_plugin; +GeanyData *geany_data; + +static GtkWidget *sidebar_vbox = NULL; +static gint page_number = -1; + +static gboolean gp_init(GeanyPlugin *plugin, G_GNUC_UNUSED gpointer data) +{ + geany_plugin = plugin; + geany_data = plugin->geany_data; + + sidebar_vbox = gtk_label_new("No active plan"); + gtk_widget_show(sidebar_vbox); + + GtkWidget *label = gtk_label_new("Progress"); + page_number = gtk_notebook_append_page( + GTK_NOTEBOOK(geany->main_widgets->sidebar_notebook), + sidebar_vbox, label); + + return TRUE; +} + +static void gp_cleanup(G_GNUC_UNUSED GeanyPlugin *plugin, + G_GNUC_UNUSED gpointer data) +{ + if (page_number >= 0) { + gtk_notebook_remove_page( + GTK_NOTEBOOK(geany->main_widgets->sidebar_notebook), + page_number); + sidebar_vbox = NULL; + page_number = -1; + } +} + +G_MODULE_EXPORT void geany_load_module(GeanyPlugin *plugin) +{ + plugin->info->name = "Progress"; + plugin->info->description = + "Shows AI agent plan progress in the sidebar. " + "Receives JSON updates over a Unix socket ($GEANY_PROGRESS_SOCK)."; + plugin->info->version = "0.1"; + plugin->info->author = "teknopaul"; + + plugin->funcs->init = gp_init; + plugin->funcs->cleanup = gp_cleanup; + + GEANY_PLUGIN_REGISTER(plugin, 235); +} +``` + +### Files to edit + +**`geany-plugins/Makefile.am`** — add after the geanyenv block: +```makefile +if ENABLE_GEANYPROGRESS +SUBDIRS += geanyprogress +endif +``` + +**`geany-plugins/configure.ac`** — add `GP_CHECK_PLUGIN(geanyprogress)` after +the geanyenv entry. (The macro handles `--enable-geanyprogress` and the +`ENABLE_GEANYPROGRESS` conditional automatically.) + +### Build & test + +```sh +cd geany-plugins +./autogen.sh +./configure --enable-geanyprogress +make -C geanyprogress +# install .la into geany's plugin dir then restart geany +``` + +Verify: "Progress" tab appears in the left sidebar. + +### Phase 1 done — write to PROGRESS.md + +```markdown +| 1 | Plugin scaffold & build system | complete | +``` + +--- + +## Phase 2 — Unix Domain Socket Server + +**Goal:** Plugin creates a socket on init, sets `GEANY_PROGRESS_SOCK`, accepts +connections, and prints received JSON to stderr. No parsing yet. UI unchanged. + +### Key GIO types + +Use `GSocketService` (GLib's high-level async socket API, GTK-thread-safe): + +```c +#include +#include +``` + +### What to add to `geanyprogress.c` + +**New module globals:** +```c +static GSocketService *sock_service = NULL; +static gchar *sock_path = NULL; +``` + +**Socket init function** (called from `gp_init`): +```c +static gboolean socket_init(void) +{ + sock_path = g_strdup_printf("/tmp/geany-progress-%d.sock", (int)getpid()); + + GSocketAddress *addr = g_unix_socket_address_new(sock_path); + sock_service = g_socket_service_new(); + + GError *err = NULL; + if (!g_socket_listener_add_address(G_SOCKET_LISTENER(sock_service), + addr, G_SOCKET_TYPE_STREAM, + G_SOCKET_PROTOCOL_DEFAULT, + NULL, NULL, &err)) { + g_warning("geanyprogress: socket bind failed: %s", err->message); + g_error_free(err); + g_object_unref(addr); + g_object_unref(sock_service); + sock_service = NULL; + g_free(sock_path); + sock_path = NULL; + return FALSE; + } + g_object_unref(addr); + + g_signal_connect(sock_service, "incoming", + G_CALLBACK(on_incoming_connection), NULL); + g_socket_service_start(sock_service); + + /* Advertise to child processes */ + g_setenv("GEANY_PROGRESS_SOCK", sock_path, TRUE); + return TRUE; +} +``` + +**Socket cleanup** (called from `gp_cleanup`): +```c +static void socket_cleanup(void) +{ + if (sock_service) { + g_socket_service_stop(sock_service); + g_object_unref(sock_service); + sock_service = NULL; + } + if (sock_path) { + g_unlink(sock_path); + g_unsetenv("GEANY_PROGRESS_SOCK"); + g_free(sock_path); + sock_path = NULL; + } +} +``` + +**Incoming connection handler:** +```c +static gboolean on_incoming_connection(G_GNUC_UNUSED GSocketService *service, + GSocketConnection *connection, + G_GNUC_UNUSED GObject *source, + G_GNUC_UNUSED gpointer data) +{ + GInputStream *in = g_io_stream_get_input_stream(G_IO_STREAM(connection)); + gchar buf[4096]; + gssize n = g_input_stream_read(in, buf, sizeof(buf) - 1, NULL, NULL); + if (n > 0) { + buf[n] = '\0'; + g_printerr("geanyprogress: received: %s\n", buf); + /* Phase 3 will parse buf here */ + } + return TRUE; +} +``` + +Note: `GSocketService` callbacks run on the GLib main loop — they are GTK-safe without +any extra locking. This is the key reason to use `GSocketService` over raw POSIX sockets +in a separate thread. + +### Build & test + +```sh +make -C geanyprogress && # install + restart geany +echo 'hello' | nc -U "$GEANY_PROGRESS_SOCK" +# Check Geany's terminal / stderr for: geanyprogress: received: hello +``` + +Also verify `echo $GEANY_PROGRESS_SOCK` inside a geanycli or geanyagent terminal shows +the socket path (child process env inheritance). + +### Phase 2 done — update PROGRESS.md + +```markdown +| 2 | Unix domain socket server | complete | +``` + +--- + +## Phase 3 — JSON Parsing + In-Memory Model + +**Goal:** Parse both message types into a `ProgressPlan` struct. Log parsed content to +stderr. No UI update yet. + +### Model structs + +```c +#define MAX_PHASES 32 + +typedef struct { + gchar *name; + gboolean done; +} Phase; + +typedef struct { + gchar *plan_name; + Phase phases[MAX_PHASES]; + gint n_phases; +} ProgressPlan; + +static ProgressPlan current_plan = {0}; +``` + +### Hand-rolled JSON parser strategy + +The two message formats are simple enough for a minimal hand-parser. +No external library needed. Key helpers needed: + +```c +/* Extract string value for a key in a flat JSON object. + * Returns newly allocated string or NULL. Non-recursive — only top-level keys. */ +static gchar *json_get_string(const gchar *json, const gchar *key); + +/* Extract integer value for a key. Returns -1 if not found. */ +static gint json_get_int(const gchar *json, const gchar *key); + +/* Extract JSON array of strings for a key. + * Fills out[] (caller-allocated) up to max entries. Returns count. */ +static gint json_get_string_array(const gchar *json, const gchar *key, + gchar **out, gint max); +``` + +Implementation approach for `json_get_string`: find `"key"`, skip `:`, skip whitespace, +read the quoted value respecting `\"` escapes. Use `g_strstr_len` and pointer arithmetic. + +### Dispatch function + +```c +static void plan_free(void) +{ + g_free(current_plan.plan_name); + for (gint i = 0; i < current_plan.n_phases; i++) + g_free(current_plan.phases[i].name); + memset(¤t_plan, 0, sizeof(current_plan)); +} + +static void handle_message(const gchar *json) +{ + /* Detect message type by key presence */ + if (strstr(json, "\"phases\"")) { + /* Init plan */ + plan_free(); + current_plan.plan_name = json_get_string(json, "plan"); + gchar *phase_names[MAX_PHASES] = {0}; + current_plan.n_phases = json_get_string_array(json, "phases", + phase_names, MAX_PHASES); + for (gint i = 0; i < current_plan.n_phases; i++) { + current_plan.phases[i].name = phase_names[i]; + current_plan.phases[i].done = FALSE; + } + } else if (strstr(json, "\"phase\"")) { + /* Update phase */ + gint idx = json_get_int(json, "phase") - 1; /* 1-based → 0-based */ + if (idx >= 0 && idx < current_plan.n_phases) + current_plan.phases[idx].done = TRUE; + } + + /* Debug: log parsed state */ + g_printerr("geanyprogress: plan='%s' phases=%d\n", + current_plan.plan_name ? current_plan.plan_name : "(none)", + current_plan.n_phases); +} +``` + +Call `handle_message(buf)` from `on_incoming_connection` instead of the printerr stub. + +### Build & test + +```sh +echo '{"plan":"My Plan","phases":["Phase 1","Phase 2","Phase 3"]}' \ + | nc -U "$GEANY_PROGRESS_SOCK" +# expect: geanyprogress: plan='My Plan' phases=3 + +echo '{"phase":1,"status":"complete"}' | nc -U "$GEANY_PROGRESS_SOCK" +# expect: geanyprogress: plan='My Plan' phases=3 (phase[0].done == TRUE) +``` + +### Phase 3 done — update PROGRESS.md + +```markdown +| 3 | JSON parsing + in-memory model | complete | +``` + +--- + +## Phase 4 — GTK Panel UI + +**Goal:** Replace the placeholder label with a `GtkTreeView` that shows the active plan +name and numbered phases with checkmarks. Refreshes live when socket messages arrive. + +### Tree model columns + +```c +enum { + COL_NUM = 0, /* gchar * — "1", "2", ... or "" for header row */ + COL_NAME = 1, /* gchar * — phase name or plan name */ + COL_STATUS = 2, /* gchar * — "✓" or "○" or "" */ + N_COLS +}; + +static GtkListStore *list_store = NULL; +static GtkWidget *tree_view = NULL; +``` + +### UI construction (replaces the `gtk_label_new` stub in `gp_init`) + +```c +static GtkWidget *build_panel(void) +{ + list_store = gtk_list_store_new(N_COLS, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_STRING); + tree_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(list_store)); + gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree_view), FALSE); + + GtkCellRenderer *r; + GtkTreeViewColumn *col; + + r = gtk_cell_renderer_text_new(); + col = gtk_tree_view_column_new_with_attributes("#", r, "text", COL_NUM, NULL); + gtk_tree_view_column_set_min_width(col, 24); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), col); + + r = gtk_cell_renderer_text_new(); + g_object_set(r, "ellipsize", PANGO_ELLIPSIZE_END, NULL); + col = gtk_tree_view_column_new_with_attributes("Phase", r, "text", COL_NAME, NULL); + gtk_tree_view_column_set_expand(col, TRUE); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), col); + + r = gtk_cell_renderer_text_new(); + col = gtk_tree_view_column_new_with_attributes("", r, "text", COL_STATUS, NULL); + gtk_tree_view_column_set_min_width(col, 24); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), col); + + GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_container_add(GTK_CONTAINER(scroll), tree_view); + gtk_widget_show_all(scroll); + return scroll; +} +``` + +### UI refresh function (called after every `handle_message`) + +```c +static void ui_refresh(void) +{ + if (!list_store) + return; + + gtk_list_store_clear(list_store); + GtkTreeIter iter; + + /* Header row — plan name */ + gtk_list_store_append(list_store, &iter); + gtk_list_store_set(list_store, &iter, + COL_NUM, "", + COL_NAME, current_plan.plan_name ? current_plan.plan_name + : "No active plan", + COL_STATUS, "", + -1); + + for (gint i = 0; i < current_plan.n_phases; i++) { + gchar *num = g_strdup_printf("%d", i + 1); + gtk_list_store_append(list_store, &iter); + gtk_list_store_set(list_store, &iter, + COL_NUM, num, + COL_NAME, current_plan.phases[i].name, + COL_STATUS, current_plan.phases[i].done ? "✓" : "○", + -1); + g_free(num); + } +} +``` + +**Thread safety note:** `on_incoming_connection` runs on the GLib main loop (not a +separate thread) because `GSocketService` uses the default main context. Therefore +calling GTK functions directly from the handler is safe — no `g_idle_add()` needed. + +Call `ui_refresh()` at the end of `handle_message()`. + +### Build & test + +Load the plugin. Send an init message. Verify the sidebar panel shows the plan name and +phase list with "○" icons. Send a done message. Verify the corresponding row shows "✓". + +```sh +echo '{"plan":"geanyprogress","phases":["Scaffold","Socket","JSON","UI","Persist","Script"]}' \ + | nc -U "$GEANY_PROGRESS_SOCK" +echo '{"phase":1,"status":"complete"}' | nc -U "$GEANY_PROGRESS_SOCK" +echo '{"phase":2,"status":"complete"}' | nc -U "$GEANY_PROGRESS_SOCK" +``` + +### Phase 4 done — update PROGRESS.md + +```markdown +| 4 | GTK panel UI | complete | +``` + +--- + +## Phase 5 — Persistence (.planning/state/xxx.md) + +**Goal:** After every model update, write a markdown progress file to the project root +under `.planning/state/.md`. Matches the format of the existing `PROGRESS.md`. + +### Slug derivation + +```c +/* Convert plan name to filesystem-safe slug: lowercase, spaces→dashes, strip rest */ +static gchar *make_slug(const gchar *name) +{ + GString *s = g_string_new(NULL); + for (const gchar *p = name; *p; p++) { + if (g_ascii_isalnum(*p)) + g_string_append_c(s, g_ascii_tolower(*p)); + else if (*p == ' ' || *p == '-' || *p == '_') + g_string_append_c(s, '-'); + } + return g_string_free(s, FALSE); +} +``` + +### Write function + +```c +static void persist_plan(void) +{ + GeanyApp *app = geany->app; + if (!app->project || !app->project->base_path) + return; + if (!current_plan.plan_name) + return; + + gchar *state_dir = g_build_filename(app->project->base_path, + ".planning", "state", NULL); + g_mkdir_with_parents(state_dir, 0755); + + gchar *slug = make_slug(current_plan.plan_name); + gchar *path = g_build_filename(state_dir, slug, NULL); + g_free(slug); + + /* Append .md if not already present */ + gchar *md_path = g_strconcat(path, ".md", NULL); + g_free(path); + + GString *content = g_string_new(NULL); + g_string_append_printf(content, "# %s — Progress\n\n", current_plan.plan_name); + g_string_append(content, "| Phase | Description | Status |\n"); + g_string_append(content, "|-------|-------------|--------|\n"); + + for (gint i = 0; i < current_plan.n_phases; i++) { + g_string_append_printf(content, "| %d | %s | %s |\n", + i + 1, + current_plan.phases[i].name, + current_plan.phases[i].done ? "complete" : "pending"); + } + + GError *err = NULL; + if (!g_file_set_contents(md_path, content->str, -1, &err)) { + g_warning("geanyprogress: write failed: %s", err->message); + g_error_free(err); + } + + g_string_free(content, TRUE); + g_free(md_path); + g_free(state_dir); +} +``` + +Call `persist_plan()` at the end of `handle_message()`, after `ui_refresh()`. + +### Build & test + +```sh +echo '{"plan":"My Test Plan","phases":["Step A","Step B"]}' \ + | nc -U "$GEANY_PROGRESS_SOCK" +cat .planning/state/my-test-plan.md +# Should show markdown table with both phases pending + +echo '{"phase":1,"status":"complete"}' | nc -U "$GEANY_PROGRESS_SOCK" +cat .planning/state/my-test-plan.md +# Step A should now show "complete" +``` + +### Phase 5 done — update PROGRESS.md + +```markdown +| 5 | Persistence (.planning/state/) | complete | +``` + +--- + +## Phase 6 — geanyenv Integration + Helper Script + +**Goal:** Emit `geanyagent-restart` so the agent VTE picks up `GEANY_PROGRESS_SOCK` +without a manual restart. Ship a `geany-progress` shell helper for clean agent calls. + +### geanyagent-restart signal + +In `socket_init()`, after `g_setenv(...)`, add: + +```c +/* Restart agent terminal so it inherits the new env var. + * Pattern taken from geanyenv.c:env_load(). */ +GType obj_type = G_OBJECT_TYPE(geany->object); +if (!g_signal_lookup("geanyagent-restart", obj_type)) + g_signal_new("geanyagent-restart", obj_type, G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, G_TYPE_NONE, 0); +if (g_signal_lookup("geanyagent-restart", obj_type)) + g_signal_emit_by_name(geany->object, "geanyagent-restart"); +``` + +This mirrors the pattern in `geanyenv.c:146-147` exactly. + +### Helper script: `geany-progress` + +Create `geanyprogress/geany-progress` (installed to `$(bindir)` or kept as a project +script that users copy to a location on `$PATH`): + +```sh +#!/bin/sh +# geany-progress — Send plan progress updates to the geanyprogress Geany plugin +# +# Usage: +# geany-progress init "Plan Name" "Phase 1" "Phase 2" ... +# geany-progress done N +# geany-progress status +# +# Requires: nc (netcat) or socat; GEANY_PROGRESS_SOCK must be set. + +set -e + +SOCK="${GEANY_PROGRESS_SOCK:-}" + +if [ -z "$SOCK" ]; then + echo "geany-progress: GEANY_PROGRESS_SOCK is not set" >&2 + exit 1 +fi + +_send() { + if command -v nc >/dev/null 2>&1; then + printf '%s' "$1" | nc -U "$SOCK" + elif command -v socat >/dev/null 2>&1; then + printf '%s' "$1" | socat - "UNIX-CONNECT:$SOCK" + else + echo "geany-progress: need nc or socat" >&2 + exit 1 + fi +} + +_json_str() { + # Minimal JSON string escape: backslash and double-quote only + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +cmd="$1" +shift || true + +case "$cmd" in + init) + plan_name="$1"; shift + json="{\"plan\":\"$(_json_str "$plan_name")\",\"phases\":[" + sep="" + for phase in "$@"; do + json="${json}${sep}\"$(_json_str "$phase")\"" + sep="," + done + json="${json}]}" + _send "$json" + ;; + done) + n="$1" + _send "{\"phase\":${n},\"status\":\"complete\"}" + ;; + status) + echo "GEANY_PROGRESS_SOCK=$SOCK" + ls -la "$SOCK" 2>/dev/null || echo "(socket not found)" + ;; + *) + echo "Usage: geany-progress init [phases...]" >&2 + echo " geany-progress done N" >&2 + echo " geany-progress status" >&2 + exit 1 + ;; +esac +``` + +Make it executable: `chmod +x geanyprogress/geany-progress`. + +To make it available in VTE terminals without a full install, the agent can set +`PATH=$PATH:/path/to/geany-plugins/geanyprogress` in `geany.env`. + +### Build & test + +Reload the plugin. Open a geanycli terminal. Verify `GEANY_PROGRESS_SOCK` is set. +Run the script end-to-end: + +```sh +geany-progress init "geanyprogress" \ + "Scaffold" "Socket" "JSON" "UI" "Persist" "Script" +geany-progress done 1 +geany-progress done 2 +geany-progress done 3 +geany-progress done 4 +geany-progress done 5 +geany-progress done 6 +``` + +Verify: sidebar updates in real time; `.planning/state/geanyprogress.md` is written. + +### Phase 6 done — update PROGRESS.md + +```markdown +| 6 | geanyenv integration + helper script | complete | +``` + +--- + +## Summary of Phases + +| Phase | Description | Key output | +|-------|------------------------------------------|-------------------------------------| +| 1 | Plugin scaffold & build system | Sidebar "Progress" tab loads | +| 2 | Unix domain socket server | `GEANY_PROGRESS_SOCK` set; nc works | +| 3 | JSON parsing + in-memory model | Both message types parsed correctly | +| 4 | GTK panel UI | Live-updating GtkTreeView | +| 5 | Persistence (.planning/state/) | Markdown file written on update | +| 6 | geanyenv integration + helper script | `geany-progress` script works | + +## Resuming after /clear + +Start a new session with: +> "Continue geanyprogress Phase N per ai-context/geanyprogress-plan.md. +> Current progress is in ai-context/PROGRESS.md." diff --git a/ai-context/geanyvosk-plan.md b/ai-context/geanyvosk-plan.md new file mode 100644 index 0000000000..c46707ebda --- /dev/null +++ b/ai-context/geanyvosk-plan.md @@ -0,0 +1,563 @@ +# geanyvosk — Voice Control Plugin for Geany +## Implementation Plan + +**Spec source:** `ai-prompts/geanyspeach.md` +**Model:** Claude Sonnet 4.6 (one phase per context window) +**Progress tracking:** `./ai-context/PROGRESS.md` + +--- + +## Architecture Overview + +A new Geany plugin `geanyvosk` (directory: `geany-plugins/geanyvosk/`) written in C. + +It runs a background thread that continuously feeds microphone audio through **Vosk** +for offline speech recognition. It operates in two modes: + +- **Idle mode** — listens only for the wake phrase "Rub lamp". CPU-cheap keyword pass. +- **Active mode** — full ASR; dispatches recognized utterances to command handlers. + Deactivated by "Back in bottle". + +Commands are routed to **geanycontrol**'s Unix socket (`~/.config/geany/geanycontrol.sock`) +for UI operations, and directly into **geanyagent**'s VTE terminal for skill execution. + +Visual state is shown via a status-bar label (active/idle indicator). + +--- + +## Dependencies + +- **Vosk** — single C API header + shared lib, runtime model download, excellent accuracy. + No distro package; install `libvosk` from https://alphacephei.com/vosk/models +- **ALSA** (`libasound2-dev`) — microphone capture at 16 kHz mono 16-bit PCM. + No PulseAudio dependency. + +--- + +## Phase 1 — Project Scaffold & Build System + +**Goal:** Bare plugin compiles and loads into Geany. No audio yet. + +### Files to create + +``` +geanyvosk/ + AUTHORS + ChangeLog + COPYING (copy from geanyagent/) + NEWS + README + Makefile.am + src/ + Makefile.am + geanyvosk.c (plugin boilerplate) + geanyvosk.h (shared constants and structs) +``` + +### Makefile.am (top-level geanyvosk/) + +```makefile +include $(top_srcdir)/build/vars.auxfiles.mk +AUXFILES = +SUBDIRS = src +plugin = geanyvosk +``` + +### src/Makefile.am + +Model on `geanyagent/src/Makefile.am`. Key additions: +```makefile +pkglib_LTLIBRARIES = geanyvosk.la +geanyvosk_la_SOURCES = geanyvosk.c +geanyvosk_la_CFLAGS = $(PLUGIN_CFLAGS) $(VOSK_CFLAGS) $(ALSA_CFLAGS) +geanyvosk_la_LIBADD = $(PLUGIN_LIBS) $(VOSK_LIBS) $(ALSA_LIBS) +geanyvosk_la_LDFLAGS = $(PLUGIN_LDFLAGS) +``` + +### configure.ac additions + +After the `geanycontrol` block, add: +```m4 +dnl geanyvosk — voice control +PKG_CHECK_MODULES([ALSA], [alsa], [], [AC_MSG_ERROR([libasound required])]) +AC_CHECK_HEADER([vosk_api.h], + [AC_CHECK_LIB([vosk], [vosk_model_new], + [VOSK_LIBS="-lvosk" && AC_DEFINE([HAVE_VOSK],[1],[Vosk ASR available])], + [AC_MSG_ERROR([libvosk not found])])], + [AC_MSG_ERROR([vosk_api.h not found])]) +AC_SUBST([VOSK_CFLAGS]) +AC_SUBST([VOSK_LIBS]) +``` + +### Makefile.am (top-level) addition + +After `geanycontrol` block: +```makefile +if ENABLE_GEANYVOSK +SUBDIRS += geanyvosk +endif +``` + +### geanyvosk.c skeleton + +```c +/* + * geanyvosk.c — Geany plugin: voice control via Vosk + */ +#include +PLUGIN_VERSION_CHECK(224) +PLUGIN_SET_INFO("GeanyVosk", "Voice control for Geany", "0.1", "teknopaul") + +GeanyPlugin *geany_plugin; +GeanyData *geany_data; + +void plugin_init(GeanyData *data) { /* Phase 2 fills this */ } +void plugin_cleanup(void) { /* Phase 2 fills this */ } +``` + +### Acceptance criteria + +- `./autogen.sh && ./configure && make` compiles without errors. +- Plugin appears in Geany Plugin Manager and can be loaded/unloaded. +- Write `PROGRESS.md` entry: Phase 1 complete. + +--- + +## Phase 2 — ALSA Microphone Capture Thread + +**Goal:** Plugin captures 16 kHz mono 16-bit PCM from default ALSA device into a +ring buffer on a background GLib thread. No ASR yet; just verify audio flows. + +### New code in geanyvosk.c + +**Structs:** +```c +#define SAMPLE_RATE 16000 +#define FRAMES_CHUNK 4000 /* 250 ms */ + +typedef struct { + snd_pcm_t *pcm; + GThread *thread; + GMutex mutex; + GCond cond; + gboolean stop; + gint16 buf[FRAMES_CHUNK * 2]; /* double-buffer */ + gsize buf_len; +} AudioCapture; +``` + +**Thread function:** +```c +static gpointer audio_thread(gpointer data) { + AudioCapture *ac = data; + /* snd_pcm_open, set_hw_params (rate=16000, channels=1, format=S16_LE) */ + /* loop: snd_pcm_readi -> push to recognizer queue -> check ac->stop */ +} +``` + +**init/cleanup:** +```c +static AudioCapture *ac = NULL; + +void plugin_init(GeanyData *d) { + ac = g_new0(AudioCapture, 1); + /* open PCM, start thread */ +} +void plugin_cleanup(void) { + ac->stop = TRUE; + g_thread_join(ac->thread); + snd_pcm_close(ac->pcm); + g_free(ac); +} +``` + +**Debug toggle:** compile with `-DVOSK_DUMP_PCM` to write raw audio to `/tmp/geanyvosk.raw` +for verification with `aplay -f S16_LE -r 16000 -c 1 /tmp/geanyvosk.raw`. + +### Acceptance criteria + +- Plugin loads; microphone capture starts without error log messages. +- With debug flag, `/tmp/geanyvosk.raw` plays back recognisably. +- Write `PROGRESS.md` entry: Phase 2 complete. + +--- + +## Phase 3 — Vosk ASR Integration & Wake Word Detection + +**Goal:** Feed captured audio into Vosk; detect "rub lamp" and "back in bottle". +Toggle `active_mode` boolean. Show state in status bar. + +### Vosk model setup (runtime, not compiled in) + +Model path: `~/.local/share/geanyvosk/model/` (e.g. `vosk-model-small-en-us`). +Plugin prints an actionable error to Geany's message window if model is absent: +``` +GeanyVosk: Vosk model not found. Download from https://alphacephei.com/vosk/models +and extract to ~/.local/share/geanyvosk/model/ +``` + +### Code additions + +```c +#include +static VoskModel *vosk_model = NULL; +static VoskRecognizer *vosk_rec = NULL; + +static gboolean active_mode = FALSE; +static GtkWidget *status_label = NULL; /* added to Geany status bar */ +``` + +**Recognizer init (called from plugin_init after audio thread starts):** +```c +static gboolean vosk_init_recognizer(void) { + gchar *model_path = g_build_filename(g_get_user_data_dir(), + "geanyvosk", "model", NULL); + vosk_model = vosk_model_new(model_path); + g_free(model_path); + if (!vosk_model) return FALSE; + vosk_rec = vosk_recognizer_new(vosk_model, (float)SAMPLE_RATE); + return TRUE; +} +``` + +**Audio thread feeds recognizer:** +```c +/* inside audio_thread loop, after snd_pcm_readi: */ +if (vosk_recognizer_accept_waveform_s(vosk_rec, + (const char *)buf, frames * 2)) { + const char *result = vosk_recognizer_result(vosk_rec); + /* parse JSON result for "text" field, dispatch to command handler */ + g_idle_add(vosk_dispatch, g_strdup(result)); +} +``` + +**Wake word / deactivate detection (in vosk_dispatch, main thread):** +```c +static gboolean vosk_dispatch(gpointer raw_json) { + gchar *text = extract_text_from_json(raw_json); + if (!active_mode) { + if (strstr(text, "rub lamp")) + vosk_set_active(TRUE); + } else { + if (strstr(text, "back in bottle")) + vosk_set_active(FALSE); + else + vosk_handle_command(text); + } + g_free(text); + g_free(raw_json); + return G_SOURCE_REMOVE; +} +``` + +**Status bar label:** +```c +static void vosk_set_active(gboolean on) { + active_mode = on; + gtk_label_set_text(GTK_LABEL(status_label), + on ? "Voice: ACTIVE" : "Voice: idle"); +} +``` + +Add `status_label` to `geany->main_widgets->statusbar` hbox in `plugin_init`. + +### Acceptance criteria + +- Say "Rub lamp" → status bar shows "Voice: ACTIVE". +- Say "Back in bottle" → status bar shows "Voice: idle". +- Unrecognised speech in active mode logs text to Geany console (debug). +- Write `PROGRESS.md` entry: Phase 3 complete. + +--- + +## Phase 4 — UI Command Dispatch via geanycontrol + +**Goal:** Recognised utterances in active mode are mapped to geanycontrol socket +commands (open-file, tab-switching, menu activation). + +### geanycontrol socket helper + +```c +/* Send one command line to geanycontrol socket; return reply or NULL */ +static gchar *ctrl_send(const gchar *cmd) { + const gchar *sock = g_build_filename(g_get_user_config_dir(), + "geany", "geanycontrol.sock", NULL); + GSocketClient *client = g_socket_client_new(); + GSocketConnection *conn = g_socket_client_connect_to_path( + client, sock, NULL, NULL); + /* write cmd + "\n", read reply line */ + /* ... */ +} +``` + +### Command table + +Map spoken phrases → geanycontrol commands. Use a simple table of +`{spoken_pattern, ctrl_command}` pairs, matched with `strstr` in order: + +| Spoken (substring match) | geanycontrol command | +|----------------------|-----------------------------| +| "switch to agent" | `switch-tab Agent` | +| "switch to cli" | `switch-tab CLI` | +| "open file \" | `open-file ` | +| "save all" | `save-all` | +| "close file" | `close-file ` | +| "refresh" | `refresh` | + +**Tab switching** — geanycontrol doesn't yet have a `switch-tab` command. Phase 4 +adds one: + +New command in `geanycontrol.c`: +``` +switch-tab