Skip to content

v0.19.3#137

Merged
runkids merged 10 commits intomainfrom
v0.19.3
Apr 19, 2026
Merged

v0.19.3#137
runkids merged 10 commits intomainfrom
v0.19.3

Conversation

@runkids
Copy link
Copy Markdown
Owner

@runkids runkids commented Apr 19, 2026

  1. Web-based skill editor — edit SKILL.md files directly in the browser with a two-pane Markdown editor, structured frontmatter fields, and a diff review step before saving feat(ui): add markdown editing #128
  2. Open in local editor — one click to open any skill in your preferred terminal editor ($EDITOR)
  3. Web UI localization — 11 languages with auto-detection from browser settings; Persian displays in right-to-left layout

runkids added 9 commits April 18, 2026 22:56
… diff review

Introduces a full in-browser skill editor reachable from the skill detail
page, replacing the previous read-only detail view for edits.

Editor
- Two-pane layout with Markdown textarea, live preview, and synced scroll
- Edit / Split / Preview mode toggle in the status bar (⌘P to cycle)
- Keyboard shortcuts: ⌘S save (with diff review), Esc cancel
- Outline drawer with jump-to-heading
- Token / word / line / file count in status bar, 5K budget warning

Frontmatter
- Grouped field editor (Identity / Invocation / Execution / Metadata)
  matching the 13 official SKILL.md fields
- Shared 1,536-char budget enforced across description + when_to_use
- Switch toggles for booleans, segmented control for enums, chip array
  inputs for list-valued fields
- Metadata group edits nested metadata.* keys directly, with stable row
  ids so typing key → clicking value no longer remounts the inputs
- Special handling for metadata.targets so values serialize as a YAML
  list (backend ParseFrontmatterList requires arrays)
- YAML mode for raw editing; Fields ↔ YAML round-trips cleanly
- Legacy root-level 'targets:' migrated to metadata.targets on load

Diff review
- Save triggers a side-by-side diff modal built on the shared DialogShell
  with focus trap and backdrop, using the Button component for actions
- YAML serializer emits plain scalars when safe instead of wrapping every
  value containing ':', '*', '#', or '"', eliminating spurious churn in
  the save diff

Misc
- DialogShell gains 4xl–7xl maxWidth presets
- Resource detail sidebar shows a Targets row when metadata.targets is set

Backend additions (server handlers):
- POST /api/resources/:name/content   save SKILL.md content
- POST /api/resources/:name/open      open in local editor
…rop dead code

- Drop unused EditorCustomField / initialCustomFields / draftTargets / FileText
  re-export; narrow onSaved signature to (nextContent: string).
- Gate DiffView render on showDiff so the O(n·m) LCS + composeSkillMarkdown
  don't run on every keystroke while the dialog is closed.
- Route the markdown preview and Outline through useDeferredValue so typing
  stays responsive while react-markdown catches up.
- Serialize frontmatter YAML lazily (only when yamlMode is active).
- Equality-guard the metadata-row reconcile effect to avoid no-op setState
  storms on every frontmatter keystroke.
- Memoize derived stats (tokens/words/lines) into a single pass instead of
  two splits per render.
- Replace the private YAML parser in ResourceDetailPage with
  lib/frontmatter's parseSkillMarkdown; removes ~60 LOC of duplicate logic.
- Remove WHAT-narrating comments ("Keyboard shortcuts", "Derived stats",
  "Stage 1: Content Stats Bar", etc.).
Follow-up to the earlier simplify pass — addresses the larger refactors
called out by the three review agents:

- Drop local SegmentedField in favour of the shared SegmentedControl used
  across the dashboard; reuse it for the Fields/YAML toggle, the
  Edit/Split/Preview mode switcher, and the enum frontmatter fields. Removes
  ~34 lines of duplicated .seg-group / .seg-btn CSS.
- Split the 80-line FrontmatterField if/else into per-type subcomponents
  (EnumField / BoolField / ArrayField / TextField) dispatched from a small
  FieldControl switch. The synthetic 'conditional' field type is gone —
  visibility is now a plain 'showWhen' predicate on a 'text' field, with an
  optional 'placeholder' override.
- Extract a shared CollapsibleGroup wrapper; both FrontmatterGroup and
  FrontmatterMetadataGroup use it, deleting the copy-pasted fm-group chrome.
- requestAnimationFrame-throttle the synced-scroll handlers. Previously
  every onScroll event (~60×/s) ran buildSyncAnchors, which measures
  scrollHeight / getComputedStyle / querySelectorAll and sometimes the DOM
  mirror. Now one pass per frame per scroll source, with RAF + timer
  cleanup on unmount.

useLineDiff consolidation was considered but skipped — computeLineDiff
returns only add/remove rows, whereas DiffView needs the eq rows for the
side-by-side view, so merging would complicate the shared hook.
resolveFieldPath() was intentionally updated in 7c91308 to treat bare
list values like "- skill-a" as resolvable keys (for short-form target
name docs lookup), but the corresponding test still asserted the old
null-returning behaviour and had been failing silently ever since.
The shared SegmentedControl has no gap between icon and label, making
Edit/Split/Preview look cramped. Revert the visual to the original
editor-themed .seg-btn/.seg-group CSS (4px icon gap, paper background,
shadow-sm active state).

Keep the DRY JSX win by introducing a small EditorSegment helper that
renders the same markup the three inline call sites previously duplicated.
Replace all hardcoded English strings in DashboardPage.tsx with t() calls
using the existing useT() hook from the custom i18n system.

Added ~80 new dashboard.* keys to en.json covering:
- Stats cards (skills, agents, targets, extras)
- Source directories section
- Star CTA reminder
- Tracked repositories (labels, toasts, confirm dialogs)
- Skill updates section (status badges, check buttons)
- Security audit section (summary stats, findings)
- Targets health section (drift warnings, sync status)
- Version status section (CLI/skill version badges)
- Quick actions (sync, audit, browse, update all)
- Toast messages with parameterized interpolation

Also renamed loop variable 't' to 'tgt' in TargetsHealthSection to
avoid shadowing the t() translation function.
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive web-based skill editor and full UI localization supporting 11 languages. Key backend enhancements include new endpoints for atomic skill content updates and opening files in local editors, alongside a more robust structured error reporting system. The frontend features a new multi-pane Markdown editor with live preview, a structured frontmatter editor, and a side-by-side diff review modal. Feedback identifies a high-severity security risk where arbitrary binaries could be executed from the system PATH, a potential path validation bug on case-insensitive filesystems, and performance concerns regarding the diff algorithm's memory footprint and excessive DOM querying during scroll synchronization.

Comment on lines +146 to +150
if _, err := exec.LookPath(requested); err == nil {
return editorCandidate{name: requested, bin: requested}, requested, nil
}
return editorCandidate{}, "", fmt.Errorf("%s not found on PATH", requested)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The pickEditor function allows executing any binary found on the system's PATH if the requested editor is not a known alias. This is a security risk as it could allow arbitrary command execution if the API endpoint is exposed or triggered via a cross-site request (e.g., if the server lacks CSRF protection). It is safer to restrict the allowed editors to the knownEditors whitelist.

		return editorCandidate{}, "", fmt.Errorf("unsupported editor: %s", requested)

Comment on lines +124 to +132
func withinDir(path, dir string) bool {
abs := filepath.Clean(path)
base := filepath.Clean(dir) + string(filepath.Separator)
// If dir is exactly the file (e.g. single-file agent), treat as within.
if abs == filepath.Clean(dir) {
return true
}
return strings.HasPrefix(abs, base)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The withinDir implementation relies on strings.HasPrefix, which is case-sensitive. This can cause issues on case-insensitive file systems (like Windows or macOS) if the path casing differs. Using filepath.Rel is a more robust way to verify that a path is contained within a directory.

func withinDir(path, dir string) bool {
	rel, err := filepath.Rel(dir, path)
	return err == nil && !strings.HasPrefix(rel, "..")
}

export function diffLines(a: string[], b: string[]): DiffOp[] {
const n = a.length;
const m = b.length;
const dp: Int32Array[] = Array.from({ length: n + 1 }, () => new Int32Array(m + 1));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The diff algorithm uses a dynamic programming table with $O(N \times M)$ space complexity. For large files (e.g., near the 2MB limit), this can consume hundreds of megabytes of memory, potentially leading to browser tab crashes. Consider using a more memory-efficient algorithm like Myers' diff or limiting the line count for diffing.

if (taMax === 0 || pvMax === 0) return null;
const lh = getTextareaLineHeight(ta);
const leftCache = leftAnchorsRef.current;
const nodes = Array.from(pv.querySelectorAll<HTMLElement>('[data-slug]'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Executing querySelectorAll on every scroll frame (via requestAnimationFrame) can be performance-intensive, especially for large documents with many headings. It is more efficient to cache the list of elements with data-slug attributes and only refresh the cache when the markdown content changes.

- pickEditor now rejects unknown editor aliases instead of executing
  arbitrary binaries from PATH (security)
- withinDir uses filepath.Rel instead of strings.HasPrefix for
  correctness on case-insensitive filesystems
@runkids runkids merged commit 8f48906 into main Apr 19, 2026
8 checks passed
@runkids runkids deleted the v0.19.3 branch April 19, 2026 01:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant