0
0
Fork 0
Commit graph

22 commits

Author SHA1 Message Date
Karstein Phobic Nyvold Kvistad
7e427e93c5 release: v0.7.0 -- phobiCS-tui v0.2 (TUI keybinds + 9-tool approve-gate)
Bumps codesys-mcp-sp21-plus to 0.7.0 (main was at 0.6.4 from
intermediate releases) and phobiCS-tui's --version output to v0.2.0.

Headline changes since v0.6.4:

  TUI:
    - browser keybinds: / (filter), o (open in editor), d (cross-
      device diff), r (rescan), ? (help overlay)
    - approve mode: v toggles unified <-> side-by-side diff
    - viewer: ST syntax highlighting (keywords/types/comments/strings)
    - statusbar: stale-mirror indicator + small-terminal resize warn

  MCP server:
    - --approve-edits now gates ALL 9 modifying tools, not just
      set_pou_code: create_pou, create_property, create_method,
      create_dut, create_gvl, create_folder, delete_object,
      rename_object, add_library

No tag created here; npm publish has to be run from your terminal
because the npm 2FA passkey can't be driven through the bash tool.
2026-04-29 08:23:57 +02:00
Karstein Phobic Nyvold Kvistad
3da09f8aba tui(viewer): ST keyword/type/comment/string syntax highlighting
tokenize(line) splits a single line into typed tokens:
  keyword (cyan)   PROGRAM, FUNCTION_BLOCK, IF/THEN/ELSE/END_IF,
                   FOR/TO/DO, VAR/END_VAR, CASE/OF, AND/OR/XOR/NOT,
                   TRUE/FALSE, EXTENDS, IMPLEMENTS, etc.
  type    (magenta) BOOL, INT, DINT, REAL, LREAL, TIME, STRING,
                   ANY_*, ARRAY, POINTER, REFERENCE, etc.
  comment (gray)   (* ... *) inline and // line-to-end
  string  (yellow) 'single' and "double" quoted
  text    (none)   everything else

Identifier matching is case-sensitive uppercase only — matches
typical IEC 61131-3 convention and avoids false positives on lower-
case identifiers like if_x. Multi-line (* ... *) comments are not
joined across lines (out-of-scope for v0.2).
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
e6d833ea1a tui(browser): d opens cross-device diff for highlighted POU
When the cursor is on a POU, d collects all OTHER devices that have
a POU with the same name.

  - 0 peers: no-op (nothing to compare)
  - exactly 1 peer: open the diff overlay immediately
  - 2+ peers: open <CrossPicker> first; user picks with j/k + Enter

The diff overlay uses computeHunks against the full file contents
(not the IMPL_SENTINEL split, since we want to see decl differences
across devices too). Esc/q closes it.
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
2a743c29fa tui(approve): v toggles unified <-> side-by-side diff
Side-by-side renders old | new in two 50% columns separated by '│'.
Consecutive del/add hunks are paired row-by-row; a longer side gets
blank rows on the shorter side. Context lines mirror on both sides.

Footer now lists 'v toggle side-by-side'.
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
f0b7852dbb tui(browser): / filter mode with live POU-name substring match
/ enters filter mode. Typed chars accumulate (case-insensitive
substring match against POU.name). Backspace removes a char. Enter
commits and stays out of input mode (filter persists). Esc clears
the filter and exits.

While the filter is active, devices auto-expand and only POUs whose
name contains the filter text are listed; devices with zero matching
POUs are hidden entirely.

Filter status line ('Filter: <text>') shows above the tree, cyan
while in input mode, dim once committed.
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
2441a898e8 tui(browser): o opens highlighted POU in $EDITOR (or VS Code)
o on a POU row spawns $EDITOR (defaulting to 'code') with the
absolute path. detached + stdio:'ignore' + unref so the editor's
lifetime is independent of the TUI; shell:true so PATH lookup works
on Windows where 'code' is a .cmd shim.

Ignored on device rows (no abs_path).
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
4d7cbdb6f6 tui(browser): r re-scans mcp-mirror/
r calls onRescan() which re-walks the project root and rerenders the
Browser with the new tree. Optional prop so tests don't have to wire
it.
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
2d73d005ef tui(browser): add ? help overlay (Esc to close)
? toggles a bordered help panel listing all keybindings. While the
panel is open, all other keys are ignored except Esc (which closes
it). The footer text gets a '? help' hint.
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
91f5713cca tui(browser): add stale-mirror indicator + resize warning
formatStaleness(mtimeMs) -> null | 'Xs' | 'Xm Ys' | 'Xh Ym'.
<ResizeWarning columns rows/> warns when terminal is below 80x20.

Browser header now shows 'mirror Xm Ys old' between the project
name and the closing rule when the mirror dir mtime is older than
STALE_THRESHOLD_MS (10s). The resize warning sits between the
header and the split view so it's the first thing the user sees
when they shrink the window.
2026-04-29 08:22:09 +02:00
Karstein Phobic Nyvold Kvistad
015bae6d65 tui: wire browser dispatch with auto-discovery + state writes
Bare 'phobiCS-tui' (or 'phobiCS-tui <path>') now enters browser mode:
walks up from cwd looking for mcp-mirror/, scans the project, and
renders the Browser composer with:
  - readPou backed by fs.readFile(absPath)
  - writeSelection backed by the atomic state-file writer (path
    resolved via stateFilePath() = LOCALAPPDATA / XDG_STATE_HOME)
  - onQuit unmount + exit 0

If no mcp-mirror/ is found anywhere upward, exits 1 with a
'Run mirror_export in CODESYS first' hint.
2026-04-28 22:24:53 +02:00
Karstein Phobic Nyvold Kvistad
b1202f60d8 tui: add Browser composer with debounced selection writes
<Browser project readPou writeSelection onQuit/> wires Tree (40%) and
Viewer (60%) into a stateful split view.

Keys: j/k nav, l/Enter/right expand a device, h/left collapse,
q quit.

Two effects fire whenever the cursor sits on a POU:
  - debounced 200 ms writeSelection() so rapid j/k presses do not
    storm the state file
  - lazy readPou() with a cancelled-flag guard so the previous POU's
    text never lands after the user has already moved on
2026-04-28 22:23:49 +02:00
Karstein Phobic Nyvold Kvistad
0aef1fe009 tui: add plain-text Viewer (highlighting deferred)
<Viewer pou text scrollTop visibleRows/> renders a POU's source as
line-numbered text (4-col padding) with a bold header
'<name>.st (<kind>, <loc> L)'. Falls back to a dim '(no POU
selected)' when pou or text is null.

No syntax highlighting in v0.1 — that's intentional and tracked as
deferred. Smoke build only; render behavior is exercised through the
browser-mode integration tests in subsequent tasks.
2026-04-28 22:22:06 +02:00
Karstein Phobic Nyvold Kvistad
9902cd6dcf tui: add Tree component (collapsible device/POU listing)
<Tree project cursorPath expanded/> renders the project as a
two-level collapsible list: device headers with POU count, and (when
expanded) POU rows with name + kind + LOC count. Cursor row marked
with a leading triangle.

devicePath()/pouPath() are exported keypath helpers used by the tree
state machine to address rows uniformly.
2026-04-28 22:21:05 +02:00
Karstein Phobic Nyvold Kvistad
357c1d11c5 tui: wire approve dispatch in bin entry
phobiCS-tui dispatcher:
- --version / -v: print version, exit 0
- approve <existing> <proposed>: read both files, render <Approve>,
  exit 0 on accept / 1 on reject / 2 on bad args or read error
- no args: print 'browser mode coming' placeholder, exit 0
- SIGTERM/SIGINT during approve: unmount + exit 1

Integration tests cover the no-TTY paths (--version, missing file,
missing args). Accept/reject keybinds are covered by the
ink-testing-library tests in Approve.test.tsx; piping stdin into a
no-TTY ink process is flaky on Windows so we don't try to
integration-test that path.

The shebang line is intentionally absent from the source; the build
script prepends one to dist/tui/index.js.
2026-04-28 22:19:51 +02:00
Karstein Phobic Nyvold Kvistad
884de61cd7 tui: add Approve component with y/n keybind
<Approve fileName oldText newText onDecision/> renders a unified diff
(+/-/space sigils, 4-col line numbers, add/del totals header) and
binds y -> accept, n/q/ESC -> reject via ink useInput.

Tests use ink-testing-library; each keystroke case awaits a microtask
flush before stdin.write so that ink's useEffect-installed 'readable'
listener has actually been attached at the moment the byte arrives.
2026-04-28 22:18:00 +02:00
Karstein Phobic Nyvold Kvistad
99a9519138 tui: add mcp-mirror auto-discovery
findProjectRoot(startDir) walks up the filesystem looking for an
mcp-mirror/ subdirectory and returns the dir that contains it. Returns
null if no mirror is found anywhere upward to the FS root.
2026-04-28 21:55:32 +02:00
Karstein Phobic Nyvold Kvistad
4d22232ec0 tui: add line-based diff hunk computation
computeHunks() returns Hunk[] (add | del | ctx) with line numbers on
the new side for add/ctx and the old side for del.

Both inputs are normalized to end with a newline before calling
jsdiff's diffLines, otherwise a missing EOF newline is treated as a
token boundary and the last line shows up as a spurious del+add
pair.
2026-04-28 21:54:20 +02:00
Karstein Phobic Nyvold Kvistad
ba40f10e6e tui: add atomic Selection JSON writer
writeSelection() emits the v1 state envelope (version, updated_at,
project_dir, device, selection{kind,name,path,abs_path}, viewer_line)
to <file>.<pid>.tmp then atomic-renames into place. mkdir -p the
parent, no .tmp left behind on success.
2026-04-28 21:52:07 +02:00
Karstein Phobic Nyvold Kvistad
461bd10a73 tui: add state-file path resolver (Windows + XDG)
Resolves the location of the cross-process TUI state file:
- Windows: %LOCALAPPDATA%/codesys-mcp/tui-state.json
- POSIX: $XDG_STATE_HOME/codesys-mcp/tui-state.json, falling back to
  $HOME/.local/state/codesys-mcp/tui-state.json

Uses path.win32 / path.posix explicitly so the POSIX branches still
produce POSIX-flavored paths when this runs on a Windows host (test
machine), keeping the unit tests host-independent.
2026-04-28 21:50:44 +02:00
Karstein Phobic Nyvold Kvistad
64ab4aeaae tui: add mirror scanner with classification + LOC
Walks mcp-mirror/<device>/ trees, classifies each .st as
PRG/FB/GVL/STRUCT/ENUM/METHOD/PROPERTY_GETTER/PROPERTY_SETTER/META/OTHER,
counts non-blank LOC, and returns a Project tree.

Property declarators (e.g. FB_Sweep/PropX.st alongside
FB_Sweep/PropX/Get.st) are detected via a Get.st/Set.st child probe so
they don't get classified as METHOD.
2026-04-28 21:49:02 +02:00
Karstein Phobic Nyvold Kvistad
961d15a389 tui: add domain types (Project, POU, Hunk, Selection) 2026-04-28 21:08:50 +02:00
Karstein Phobic Nyvold Kvistad
2ea39cb9a6 tui: scaffold phobiCS-tui ESM subpackage with ink 2026-04-28 20:52:02 +02:00