Skip to content

Comments

refactor: プラグイン設定をmanifest.jsonから動的レンダリングに変更#93

Merged
oboroge0 merged 7 commits intomainfrom
refactor/dynamic-plugin-settings
Feb 19, 2026
Merged

refactor: プラグイン設定をmanifest.jsonから動的レンダリングに変更#93
oboroge0 merged 7 commits intomainfrom
refactor/dynamic-plugin-settings

Conversation

@oboroge0
Copy link
Owner

@oboroge0 oboroge0 commented Feb 18, 2026

概要

新規プラグイン追加時にフロントエンドのコード変更が一切不要になるよう、ハードコードされた設定マップを plugin store からの動的読み込みに変更しました。

  • ConfigField 型を拡張し、全カスタムフィールド型(prompt-builder, input-list, animation-file, model-file, png-expression-map 等)をサポート
  • configUtils.ts を新規作成: manifest config → NodeField 変換、showWhen 評価ユーティリティ
  • NodeSettings.tsx を plugin store 優先の動的スキーマ解決に変更(既存 nodeConfigs はフォールバックとして維持)
  • i18n キーマップをハードコードから node ID の自動 camelCase 変換に置き換え
  • FieldSelectorNode / DataPreviewPopup の出力フィールドを plugin store から動的取得
  • Canvas.tsx の動的ポート生成を manifest の config フィールド型による汎用判定に変更
  • 各プラグインの manifest.json にカスタムフィールド型と showWhen 条件を追加

変更の動機

コミュニティからのプラグインPR(例: #82 minimax-llm)で、plugins/ フォルダ内の変更だけでなく NodeSettings.tsx, Canvas.tsx, FieldSelectorNode.tsx 等の複数フロントエンドファイルへの登録追加が必要だった。この変更により、新規プラグインは manifest.json + node.ts のみで完結する。

テスト計画

  • npm run lint でエラーなし確認
  • npm test で変更起因の失敗なし確認
  • エディタで各ノードの設定パネルが正しく表示されること
    • openai-llm: API Key, Model, System Prompt, Prompt Builder, Temperature, Max Tokens, Reasoning Effort (showWhen)
    • voicevox-tts: Host, Speaker (動的ロード), Speed, Pitch, Volume
    • avatar-configuration: Renderer 選択で表示フィールドが切り替わる
    • emotion-analyzer: Method 選択で LLM/Rule-based フィールドが切り替わる
  • Prompt Builder の動的ポート生成が動作すること
  • FieldSelector ノードで上流ノードの出力フィールド候補が表示されること

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Editor now derives node ports, field schemas, and output lists from plugin manifests (including prompt-builder and input-list); plugin outputs are used for field selection and previews.
  • Improvements

    • Added conditional visibility (showWhen), dynamic option sources, and new field types (animation-file, model-file, png-expression-map, textarea, password, prompt-builder, input-list).
    • Added shared fallback output mappings and a plugin-store API to surface plugin configs and fields.

新規プラグイン追加時にフロントエンドのコード変更が不要になるよう、
ハードコードされた設定マップをplugin storeからの動的読み込みに変更。

- ConfigField型を拡張し、全カスタムフィールド型(prompt-builder, input-list等)をサポート
- configUtils.tsを新規作成: manifest config → NodeField変換、showWhen評価
- NodeSettings.tsxをplugin store優先の動的スキーマ解決に変更(nodeConfigsはフォールバック)
- i18nキーマップをnode IDからの自動導出に置き換え
- FieldSelectorNode/DataPreviewPopupの出力フィールドをplugin storeから動的取得
- Canvas.tsxの動的ポート生成をmanifestのconfigフィールド型による汎用判定に変更
- 各プラグインのmanifest.jsonにカスタムフィールド型とshowWhen条件を追加

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Editor UI now derives node ports, fields, and dynamic options from plugin manifests via new types and config utilities; pluginStore exposes plugin configs; editor components prefer plugin-provided inputs/outputs with fallbacks; several plugin manifests adopt new field types and showWhen rules.

Changes

Cohort / File(s) Summary
Type System & Config Utils
apps/web/lib/types.ts, apps/web/lib/configUtils.ts
Expanded manifest typings (new field types, ShowWhenCondition, NodeField). Added manifestConfigToNodeFields, evaluateShowWhen, normalizeOptions and type mapping to normalize manifest configs and conditional visibility.
Plugin Store API
apps/web/stores/pluginStore.ts
Added getPluginConfig(id) to expose a plugin's config (Record<string, ConfigField>
Editor Canvas / Dynamic Ports
apps/web/components/editor/Canvas.tsx
Moved dynamic port generation into render path; inspects plugin manifest fields (e.g., prompt-builder, input-list) to build/override nodeInputs; removed redundant plugin lookup and prior per-type port logic.
Node Settings Panel
apps/web/components/panels/NodeSettings.tsx
Switched to imported NodeField and manifestConfigToNodeFields/evaluateShowWhen; supports dynamic/dependsOn, asset/model loading via manifest types, and plugin-driven labels/placeholders.
Field Resolution & Previews
apps/web/components/editor/DataPreviewPopup.tsx, apps/web/components/editor/FieldSelectorNode.tsx
Prefer plugin-derived output fields via getPluginOutputs; fall back to shared nodeOutputFields when plugin data absent; removed local hardcoded maps.
Shared Fallbacks
apps/web/lib/nodeOutputFields.ts
Added exported nodeOutputFields mapping as fallback metadata for known node output fields.
Plugin Manifest Updates
plugins/.../manifest.json (avatar-configuration, http-request, motion-trigger, openai-llm, text-transform, voicevox-tts)
Manifests updated with new/changed field types (model-file, animation-file, png-expression-map, prompt-builder, input-list), added showWhen rules, headers/template -> textarea, added promptSections/templateInputs, and made VOICEVOX speaker a dynamic select dependent on host.

Sequence Diagram(s)

sequenceDiagram
    participant Canvas as Canvas Component
    participant Store as Plugin Store
    participant Utils as Config Utils
    participant Renderer as Node Renderer

    Canvas->>Store: getPluginConfig(node.type)
    Store-->>Canvas: config (Record<string,ConfigField>|undefined)
    Canvas->>Utils: manifestConfigToNodeFields(config)
    Utils-->>Canvas: NodeField[] (normalized)
    Canvas->>Canvas: inspect NodeField types (prompt-builder / input-list / etc.)
    Canvas->>Renderer: override nodeInputs -> generated ports
    Renderer-->>Canvas: rendered node with dynamic ports
Loading
sequenceDiagram
    participant NodeSettings as NodeSettings Panel
    participant Store as Plugin Store
    participant Utils as Config Utils
    participant Field as Field Renderer

    NodeSettings->>Store: getPluginConfig(node.type)
    Store-->>NodeSettings: config
    NodeSettings->>Utils: manifestConfigToNodeFields(config)
    Utils-->>NodeSettings: fields: NodeField[]
    loop each field
        NodeSettings->>Utils: evaluateShowWhen(field.showWhen, currentConfig)
        Utils-->>NodeSettings: visible (true/false)
        alt visible
            NodeSettings->>Field: request dynamic options (dependsOn / dynamic)
            Field-->>NodeSettings: renderable field UI
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Poem

🐰 I nibble manifests by starlit code,

Ports wake up where plugin whispers showed,
Fields bend to rules of showWhen and depends,
Configs now speak, and rendering amends,
The canvas hops — new inputs on the road! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is in Japanese and describes a refactor to change plugin configuration rendering from manifest.json to be dynamic, which aligns with the major theme of the PR: centralizing dynamic plugin configuration through manifest files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/dynamic-plugin-settings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (6)
plugins/openai-llm/manifest.json (1)

128-131: promptSections is always visible alongside systemPrompt — add a description for clarity.

Users see both systemPrompt and promptSections at the same time with no explanation of how they interact. At minimum, a description field would guide users. If these are mutually exclusive, a showWhen condition on one would reduce confusion.

♻️ Proposed addition
   "promptSections": {
     "type": "prompt-builder",
-    "label": "Prompt Builder"
+    "label": "Prompt Builder",
+    "description": "Compose a dynamic prompt from static text blocks and named input ports. Overrides the simple prompt input when input-type sections are defined."
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/openai-llm/manifest.json` around lines 128 - 131, The schema
currently defines "promptSections" (type: "prompt-builder") but has no
description and is always shown alongside "systemPrompt", causing user
confusion; add a "description" string to the "promptSections" entry explaining
how it relates to "systemPrompt" (e.g., when to use each and that they are
alternatives), and if they are mutually exclusive add a "showWhen" condition on
either "promptSections" or "systemPrompt" (use the existing condition syntax
used elsewhere in the manifest) so only the appropriate input is visible at a
time.
apps/web/components/editor/Canvas.tsx (2)

245-245: getPluginById is called twice per node in the same useMemo pass — unify into one call.

pluginForPorts (Line 245) and plugin (Line 287) are both getPluginById(node.type). Since both references are used within the same map iteration, this is a redundant store lookup per node. Combine them:

♻️ Proposed fix
-       // Dynamic port generation based on manifest config field types
-       const pluginForPorts = getPluginById(node.type);
-       if (pluginForPorts?.config && node.config) {
-         for (const [fieldKey, fieldDef] of Object.entries(pluginForPorts.config)) {
+       // Get plugin once for both dynamic port generation and category mapping
+       const plugin = getPluginById(node.type);
+       if (plugin?.config && node.config) {
+         for (const [fieldKey, fieldDef] of Object.entries(plugin.config)) {
            // ... dynamic port generation ...
          }
        }

        // ... get outputs ...

-       // Get category from plugin or fall back to legacy function
-       const plugin = getPluginById(node.type);
        const category = plugin?.category
          ? mapPluginCategoryToLegacy(plugin.category)
          : getNodeCategory(node.type);

Also applies to: 287-290

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/editor/Canvas.tsx` at line 245, The code calls
getPluginById(node.type) twice for the same node; inside the useMemo map, call
getPluginById(node.type) once and assign it to a single local variable (e.g.,
plugin) and then use that variable for both pluginForPorts and plugin usages;
update any places referencing pluginForPorts (line where it's declared) and the
later plugin reference (lines ~287-290) to use the single variable so the lookup
is not duplicated.

826-855: getNodeInputs hardcoded fallback for openai-llm / text-transform now partially duplicates the dynamic block — long-term maintenance risk.

The new dynamic port generation block (Lines 244–272) handles prompt-builder and input-list fields generically from the manifest. However, getNodeInputs at Lines 827–855 still has hardcoded plugin-specific branches for the exact same cases. These two paths coexist and must be kept in sync manually:

  • The dynamic block runs only when the plugin is loaded (pluginForPorts?.config is truthy).
  • getNodeInputs runs only as fallback when the plugin is not yet loaded (pluginInputs.length === 0).

This is a maintenance trap: adding a new prompt-builder or input-list plugin requires updating getNodeInputs to match. The long-term direction should be to remove the hardcoded cases from getNodeInputs once the plugin store is reliably populated at app startup.

Consider whether these hardcoded plugin-specific blocks in getNodeInputs can be removed once the plugin loading is guaranteed to complete before the canvas renders (e.g., via a loading gate). At minimum, add a comment in getNodeInputs linking it to the dynamic block so the relationship is clear.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/editor/Canvas.tsx` around lines 826 - 855, getNodeInputs
currently contains hardcoded branches for 'openai-llm' (promptSections) and
'text-transform' (templateInputs) that duplicate the dynamic port generation
implemented elsewhere (the plugin-driven logic that reads prompt-builder /
input-list from manifests), creating a maintenance trap; remove those
plugin-specific branches from getNodeInputs (leave only a generic default
fallback like a single 'prompt' or 'text' port) so port generation is
centralized in the plugin-driven code (the code that uses
pluginForPorts/pluginInputs), or if you cannot remove them yet, add a clear TODO
comment in getNodeInputs referencing the dynamic block and the manifest keys
('prompt-builder'/'input-list') and note to delete these branches once plugin
loading is guaranteed at startup (ensure references to getNodeInputs,
promptSections, templateInputs, pluginForPorts, and pluginInputs are kept in the
comment).
plugins/text-transform/manifest.json (1)

54-57: templateInputs is unconditionally visible — consider adding showWhen

The templateInputs field has no showWhen condition, so it will appear for every operation (uppercase, lowercase, trim, etc.) even though it is only meaningful when operation === "template". The same PR already uses showWhen for reasoningEffort in openai-llm, so the infrastructure is in place. Also, a description property would bring this field in line with every other config entry in this file.

♻️ Proposed fix
   "templateInputs": {
     "type": "input-list",
-    "label": "Template Inputs"
+    "label": "Template Inputs",
+    "description": "Named inputs injected as {{name}} variables in the template",
+    "showWhen": {
+      "field": "operation",
+      "operator": "equals",
+      "value": "template"
+    }
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/text-transform/manifest.json` around lines 54 - 57, Add a conditional
visibility rule and description to the existing templateInputs config: update
the templateInputs entry to include a showWhen that checks the operation field
equals "template" (so templateInputs only appears for operation === "template")
and add a descriptive description property consistent with other entries;
reference the templateInputs symbol and the operation field when making this
change.
apps/web/components/editor/DataPreviewPopup.tsx (1)

19-37: nodeOutputFields is duplicated across DataPreviewPopup.tsx and FieldSelectorNode.tsx — extract to a shared constant.

The two hardcoded fallback maps are already out of sync: FieldSelectorNode.tsx includes 'data-formatter', 'template-editor', 'switch', 'foreach' entries that are absent here, and the 'http-request' entry in both files includes a 'headers' field that the plugin manifest does not define as an output. As plugin coverage grows, these maps will continue to diverge silently.

Extract the shared constant to e.g. apps/web/lib/nodeOutputFields.ts:

♻️ Proposed refactor
// apps/web/lib/nodeOutputFields.ts  (new file)
+export const FALLBACK_NODE_OUTPUT_FIELDS: Record<string, string[]> = {
+  'twitch-chat': ['text', 'author', 'message'],
+  'youtube-chat': ['text', 'author', 'message'],
+  'manual-input': ['text'],
+  'openai-llm': ['response'],
+  'anthropic-llm': ['response'],
+  'google-llm': ['response'],
+  'ollama-llm': ['response'],
+  'timer': ['tick', 'count'],
+  'http-request': ['response', 'status'],
+  'text-transform': ['result'],
+  'data-formatter': ['formatted', 'parsed'],
+  'field-selector': ['output'],
+  'template-editor': ['output'],
+  'random': ['value'],
+  'variable': ['value'],
+  'switch': ['value', 'data'],
+  'delay': ['output'],
+  'loop': ['index', 'value'],
+  'foreach': ['item', 'index'],
+};

Then in both DataPreviewPopup.tsx and FieldSelectorNode.tsx:

-// Known output fields for each node type
-const nodeOutputFields: Record<string, string[]> = { ... };
+import { FALLBACK_NODE_OUTPUT_FIELDS } from '@/lib/nodeOutputFields';

Also applies to: 107-111

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/editor/DataPreviewPopup.tsx` around lines 19 - 37, The
duplicate fallback map nodeOutputFields should be extracted to a single shared
constant and both DataPreviewPopup and FieldSelectorNode should import it:
create a new module (e.g., apps/web/lib/nodeOutputFields.ts) exporting the
nodeOutputFields object, update DataPreviewPopup and FieldSelectorNode to remove
their local nodeOutputFields maps and import the shared constant, and reconcile
entries (remove the nonexistent 'headers' output from 'http-request' and ensure
the shared map includes 'data-formatter', 'template-editor', 'switch', and
'foreach' as present in FieldSelectorNode) so both consumers use the same source
of truth.
apps/web/lib/types.ts (1)

152-154: Consider making operator optional in the modern ShowWhenCondition variant.

Currently operator is required (operator: string), but the implementation in evaluateShowWhen doesn't use it, and all existing manifests use the legacy { key, value } form. If operator-based evaluation is deferred, marking it optional (operator?: string) avoids forcing callers to supply a value that is currently ignored, and keeps the contract honest.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/types.ts` around lines 152 - 154, Update the ShowWhenCondition
type so the modern variant doesn't require an unused operator: change the `{
field: string; operator: string; value: string | string[] }` variant to make
operator optional (`operator?: string`), and then ensure any code using
evaluateShowWhen and other consumers of ShowWhenCondition (e.g.,
evaluateShowWhen) compiles with operator possibly undefined (handle or ignore
the undefined case as current logic does).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/components/editor/Canvas.tsx`:
- Around line 244-272: The code in Canvas.tsx sets nodeInputs by casting
node.config[fieldKey] to PromptSection[] and string[] without verifying it's an
array; update the logic inside the loop that inspects pluginForPorts.config (the
block handling fieldDef.type === 'prompt-builder' and 'input-list') to first
check Array.isArray(node.config[fieldKey]) before treating it as an array, only
then map to nodeInputs using formatPortLabel and PortType; if the value is not
an array, skip that field (or fallback to an empty array) so getPluginById,
node.config, PromptSection, formatPortLabel and nodeInputs are protected from
TypeError at render time.

In `@apps/web/components/panels/NodeSettings.tsx`:
- Around line 1940-1969: The dynamic-LLM branch is too broad: it's triggered for
any field with field.dynamic && field.dependsOn and can accidentally treat
non-LLM selects as LLM model pickers; narrow the condition to only run for
actual LLM model fields (e.g., check field.key === "llm_model" || field.key ===
"model" or introduce and check a new field.optionsSource === "llm" on the
field), then use getPluginById(providerPluginId) and LLM_MODEL_OPTIONS only when
that LLM-specific indicator is present; update any field definitions that should
use this branch to include the new hint if you add one.

In `@apps/web/lib/configUtils.ts`:
- Around line 47-61: manifestConfigToNodeFields currently drops manifest
constraint properties; update the mapper (manifestConfigToNodeFields) to include
min, max, required, and default when converting a ConfigField to a NodeField so
the UI can render constraints and defaults; specifically add min: field.min,
max: field.max, required: field.required, and default: field.default to the
returned object (leaving normalizeOptions(field.options) and other keys
unchanged) and ensure any type mapping (mapFieldType) still applies.
- Around line 67-86: The function evaluateShowWhen currently ignores
condition.operator; update it to dispatch on condition.operator (read
condition.operator) inside evaluateShowWhen so the visibility logic matches the
ShowWhenCondition type: extract fieldKey (from condition.key or
condition.field), normalize expectedValues as Array, compute currentValue =
String(config[fieldKey] ?? ''), then switch on condition.operator (treat
undefined as "in" for backward compatibility) and return true for "in"/"equals"
when expectedValues includes currentValue (or equals when you want single-value
semantics), return the inverse for "not-in"/"not-equals", and throw or default
to false for unknown operators; reference evaluateShowWhen, ShowWhenCondition,
condition.operator, expectedValues, currentValue in your changes.

---

Nitpick comments:
In `@apps/web/components/editor/Canvas.tsx`:
- Line 245: The code calls getPluginById(node.type) twice for the same node;
inside the useMemo map, call getPluginById(node.type) once and assign it to a
single local variable (e.g., plugin) and then use that variable for both
pluginForPorts and plugin usages; update any places referencing pluginForPorts
(line where it's declared) and the later plugin reference (lines ~287-290) to
use the single variable so the lookup is not duplicated.
- Around line 826-855: getNodeInputs currently contains hardcoded branches for
'openai-llm' (promptSections) and 'text-transform' (templateInputs) that
duplicate the dynamic port generation implemented elsewhere (the plugin-driven
logic that reads prompt-builder / input-list from manifests), creating a
maintenance trap; remove those plugin-specific branches from getNodeInputs
(leave only a generic default fallback like a single 'prompt' or 'text' port) so
port generation is centralized in the plugin-driven code (the code that uses
pluginForPorts/pluginInputs), or if you cannot remove them yet, add a clear TODO
comment in getNodeInputs referencing the dynamic block and the manifest keys
('prompt-builder'/'input-list') and note to delete these branches once plugin
loading is guaranteed at startup (ensure references to getNodeInputs,
promptSections, templateInputs, pluginForPorts, and pluginInputs are kept in the
comment).

In `@apps/web/components/editor/DataPreviewPopup.tsx`:
- Around line 19-37: The duplicate fallback map nodeOutputFields should be
extracted to a single shared constant and both DataPreviewPopup and
FieldSelectorNode should import it: create a new module (e.g.,
apps/web/lib/nodeOutputFields.ts) exporting the nodeOutputFields object, update
DataPreviewPopup and FieldSelectorNode to remove their local nodeOutputFields
maps and import the shared constant, and reconcile entries (remove the
nonexistent 'headers' output from 'http-request' and ensure the shared map
includes 'data-formatter', 'template-editor', 'switch', and 'foreach' as present
in FieldSelectorNode) so both consumers use the same source of truth.

In `@apps/web/lib/types.ts`:
- Around line 152-154: Update the ShowWhenCondition type so the modern variant
doesn't require an unused operator: change the `{ field: string; operator:
string; value: string | string[] }` variant to make operator optional
(`operator?: string`), and then ensure any code using evaluateShowWhen and other
consumers of ShowWhenCondition (e.g., evaluateShowWhen) compiles with operator
possibly undefined (handle or ignore the undefined case as current logic does).

In `@plugins/openai-llm/manifest.json`:
- Around line 128-131: The schema currently defines "promptSections" (type:
"prompt-builder") but has no description and is always shown alongside
"systemPrompt", causing user confusion; add a "description" string to the
"promptSections" entry explaining how it relates to "systemPrompt" (e.g., when
to use each and that they are alternatives), and if they are mutually exclusive
add a "showWhen" condition on either "promptSections" or "systemPrompt" (use the
existing condition syntax used elsewhere in the manifest) so only the
appropriate input is visible at a time.

In `@plugins/text-transform/manifest.json`:
- Around line 54-57: Add a conditional visibility rule and description to the
existing templateInputs config: update the templateInputs entry to include a
showWhen that checks the operation field equals "template" (so templateInputs
only appears for operation === "template") and add a descriptive description
property consistent with other entries; reference the templateInputs symbol and
the operation field when making this change.

- Canvas.tsx: Array.isArray チェック追加、getPluginById 重複呼び出し解消
- Canvas.tsx: getNodeInputs からハードコード分岐を削除(動的ポート生成に統一)
- NodeSettings.tsx: 動的LLMセレクト条件を field.key === "model" に限定
- configUtils.ts: manifestConfigToNodeFields に min/max/required/defaultValue 追加
- configUtils.ts: evaluateShowWhen で operator (eq/neq/in) をディスパッチ
- types.ts: ShowWhenCondition の operator をオプショナルに、NodeField に min/max/required/defaultValue 追加
- nodeOutputFields を共有定数として抽出 (FieldSelectorNode, DataPreviewPopup)
- openai-llm manifest: promptSections に description 追加
- text-transform manifest: templateInputs に showWhen と description 追加

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/components/panels/NodeSettings.tsx (1)

1915-1926: ⚠️ Potential issue | 🟡 Minor

min, max, and required from NodeField are mapped but never applied as HTML attributes on the number input.

NodeField now has min/max/required/defaultValue (added as part of the previous fix), and manifestConfigToNodeFields correctly populates them from manifests (e.g., voicevox-tts speed: 0.5–2.0, pitch: −0.15–0.15). However, the "number" case in renderField does not pass these as HTML attributes, so the browser imposes no bounds and the user receives no feedback.

🛠 Proposed fix
  case "number":
    return (
      <input
        type="number"
        value={value as number}
        onChange={(e) =>
          handleChange(field.key, e.target.value === "" ? undefined : parseFloat(e.target.value))
        }
        placeholder={field.placeholder}
+       min={field.min}
+       max={field.max}
+       required={field.required}
        style={inputStyle}
      />
    );

defaultValue should also be considered during localConfig initialization:

// In useEffect where localConfig is set:
- setLocalConfig(selectedNode.config || {});
+ const base = selectedNode.config || {};
+ // Apply field defaults for missing keys
+ const withDefaults = fields.reduce((acc, f) => {
+   if (!(f.key in base) && f.defaultValue !== undefined) acc[f.key] = f.defaultValue;
+   return acc;
+ }, { ...base });
+ setLocalConfig(withDefaults);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/panels/NodeSettings.tsx` around lines 1915 - 1926, The
"number" branch in renderField does not apply NodeField's
min/max/required/defaultValue to the rendered <input>; update the number case in
renderField to pass field.min, field.max, and field.required as HTML attributes
(e.g., min={field.min} max={field.max} required={field.required}) and set the
input's value fallback to field.defaultValue when localConfig value is
undefined; also ensure localConfig initialization logic (where localConfig is
created/seeded) uses field.defaultValue for number fields so the UI and
validation reflect manifest-provided bounds (refer to renderField, NodeField,
and the localConfig initialization code paths).
🧹 Nitpick comments (1)
apps/web/lib/configUtils.ts (1)

7-17: opt.value as string | number silently coerces non-string/number option values.

ConfigField['options'] declares value: unknown, so a manifest providing value: true (boolean) or value: null passes this cast at compile time but remains the original type at runtime. The NodeField.options type only accepts string | number, yet the cast hides the mismatch instead of normalising it.

♻️ Proposed fix – normalise explicitly
- return { label: opt.label, value: opt.value as string | number };
+ const v = opt.value;
+ return {
+   label: opt.label,
+   value: typeof v === 'number' ? v : String(v ?? ''),
+ };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/configUtils.ts` around lines 7 - 17, The current
normalizeOptions function casts opt.value to string|number which hides runtime
mismatches between ConfigField['options'] (value: unknown) and
NodeField.options; update normalizeOptions to perform runtime normalization
instead: in the branch for object options (the one using opt.label/opt.value),
read const v = opt.value and if typeof v === 'string' || typeof v === 'number'
use v as the value, otherwise convert v to a string via String(v) (and treat
null/undefined as '' if you want empty string) so the returned { label, value }
always conforms to NodeField.options; keep the string-option branch unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/components/editor/Canvas.tsx`:
- Around line 248-257: In Canvas.tsx inside the block that handles fieldDef.type
=== 'prompt-builder', validate each PromptSection.content before creating ports:
change the logic that builds nodeInputs (currently using inputSections.map with
id: section.content) to first filter out sections where section.content is falsy
or empty (e.g., === ''), then map to ports; do the same defensive filter for the
'input-list' handling later so you never create a port with id: "" (refer to
node.config[fieldKey], PromptSection, inputSections and the code that assigns
nodeInputs).

In `@apps/web/components/panels/NodeSettings.tsx`:
- Around line 1833-1845: The effect that checks
getPluginById(selectedNode.type)?.config is missing a dependency on the plugin
store load status, causing fetchAnimations()/fetchModels() to be skipped if
plugins finish loading after node selection; update the dependency array for the
effect that references selectedNode/getPluginById to also include the
plugins-loaded flag (e.g., isLoaded or isPluginsLoaded from the plugin store) so
the effect re-runs once plugins are populated and triggers fetchAnimations() and
fetchModels() when the plugin config contains "animation-file" or "model-file".
- Around line 1941-1951: The dynamic model-select branch only runs when
field.key === "model", so fields like emotion-analyzer's "llm_model" never get
the dynamic options; update the conditional in NodeSettings (the block
referencing field.dynamic, field.dependsOn, field.key, localConfig,
providerPluginId, getPluginById, dynamicModelOptions, and LLM_MODEL_OPTIONS) to
also accept other LLM field names—e.g., change the check to include
field.key.includes("model") or explicitly allow "llm_model"—so
providerPlugin/modelOptions resolution runs for those keys and the select
renders dynamic options.

In `@apps/web/lib/configUtils.ts`:
- Around line 92-103: The switch on operator currently treats any unknown
operator (like "not-in") as the default "in" case which reverses intended
behavior; update the switch to explicitly handle 'in' and 'not-in' (e.g., add a
case 'in' that returns expectedValues.includes(currentValue) and a case 'not-in'
that returns !expectedValues.includes(currentValue)), and make the default
branch throw or log a clear error for unsupported operators instead of falling
through; reference the existing operator variable and the
expectedValues/currentValue comparisons when implementing these cases.

In `@apps/web/lib/nodeOutputFields.ts`:
- Around line 6-26: The fallback mapping in nodeOutputFields is out of sync with
the canvas port IDs returned by getNodeOutputs (used by DataPreviewPopup and
FieldSelectorNode); update nodeOutputFields so the keys match getNodeOutputs
exactly: change 'timer' to ['tick','timestamp'], change 'switch' to
['true','false'], change 'loop' to ['loop','done'], change 'foreach' to include
['item','index','done'], and remove 'headers' from 'http-request' so it only
lists ['response','status']; verify these entries in nodeOutputFields align with
the port IDs in Canvas.tsx/getNodeOutputs.

---

Outside diff comments:
In `@apps/web/components/panels/NodeSettings.tsx`:
- Around line 1915-1926: The "number" branch in renderField does not apply
NodeField's min/max/required/defaultValue to the rendered <input>; update the
number case in renderField to pass field.min, field.max, and field.required as
HTML attributes (e.g., min={field.min} max={field.max}
required={field.required}) and set the input's value fallback to
field.defaultValue when localConfig value is undefined; also ensure localConfig
initialization logic (where localConfig is created/seeded) uses
field.defaultValue for number fields so the UI and validation reflect
manifest-provided bounds (refer to renderField, NodeField, and the localConfig
initialization code paths).

---

Nitpick comments:
In `@apps/web/lib/configUtils.ts`:
- Around line 7-17: The current normalizeOptions function casts opt.value to
string|number which hides runtime mismatches between ConfigField['options']
(value: unknown) and NodeField.options; update normalizeOptions to perform
runtime normalization instead: in the branch for object options (the one using
opt.label/opt.value), read const v = opt.value and if typeof v === 'string' ||
typeof v === 'number' use v as the value, otherwise convert v to a string via
String(v) (and treat null/undefined as '' if you want empty string) so the
returned { label, value } always conforms to NodeField.options; keep the
string-option branch unchanged.

oboroge0 and others added 2 commits February 19, 2026 06:10
- Canvas.tsx: PromptSection.content が空の場合をフィルタ、input-list も同様
- NodeSettings.tsx: useEffect に isPluginsLoaded 依存追加(プラグインロード後の再実行)
- NodeSettings.tsx: 動的モデルセレクトを field.key.includes("model") に拡張(llm_model 対応)
- NodeSettings.tsx: number 入力に min/max/required/defaultValue を反映
- configUtils.ts: evaluateShowWhen に not-in 演算子追加、未知の演算子を警告
- configUtils.ts: normalizeOptions でランタイム型検証(unsafe cast 除去)
- nodeOutputFields.ts: Canvas.tsx の getNodeOutputs と同期(timer, switch, loop, foreach, http-request)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/components/panels/NodeSettings.tsx (1)

1890-1890: ⚠️ Potential issue | 🟡 Minor

field.defaultValue is unreachable — number inputs never show manifest defaults.

Line 1890 sets value = localConfig[field.key] ?? "". Because ?? only nullish-coalesces null/undefined, when the key is missing from localConfig, value becomes "" (an empty string, not nullish). On line 1919, (value ?? field.defaultValue ?? "") therefore always returns "" and never reaches field.defaultValue.

The commit message ("number 入力での defaultValue 反映") indicates this was intentional, but the ?? chain is subtly broken for this case.

🛠️ Proposed fix
 case "number":
   return (
     <input
       type="number"
-      value={(value ?? field.defaultValue ?? "") as number}
+      value={(value !== "" ? value : (field.defaultValue ?? "")) as number}
       onChange={(e) =>
         handleChange(field.key, e.target.value === "" ? undefined : parseFloat(e.target.value))
       }
       placeholder={field.placeholder}
       min={field.min}
       max={field.max}
       required={field.required}
       style={inputStyle}
     />
   );

Also applies to: 1919-1919

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/panels/NodeSettings.tsx` at line 1890, The bug is that
value is set with a fallback to the empty string (const value =
localConfig[field.key] ?? ""), which prevents the later nullish-coalesce (value
?? field.defaultValue ?? "") from ever using field.defaultValue; change value so
it is undefined when the key is missing instead of "", e.g. compute value using
a presence check like const value = field.key in localConfig ?
localConfig[field.key] : undefined (or use undefined/null as the fallback) so
(value ?? field.defaultValue ?? "") can correctly fall back to
field.defaultValue; update usages referencing value/localConfig/field.key
accordingly.
🧹 Nitpick comments (1)
apps/web/components/panels/NodeSettings.tsx (1)

2454-2456: toPluginCamel closes over nothing — move it outside the component.

The function is recreated on every render without needing any component state or props. Hoisting it to module scope eliminates the unnecessary allocation.

♻️ Proposed refactor
+// Convert kebab-case/snake_case to camelCase for i18n key derivation
+const toPluginCamel = (id: string) =>
+  id.replace(/[-_]([a-z0-9])/g, (_, c: string) => c.toUpperCase());
+
 export default function NodeSettings() {
   ...
-  // Convert kebab-case/snake_case to camelCase for i18n key derivation
-  const toPluginCamel = (id: string) =>
-    id.replace(/[-_]([a-z0-9])/g, (_, c: string) => c.toUpperCase());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/panels/NodeSettings.tsx` around lines 2454 - 2456, The
helper toPluginCamel is defined inside the NodeSettings component and is
recreated on every render; move this pure function to module scope (above the
component) so it is hoisted and not reallocated each render. Cut the const
toPluginCamel = (id: string) => id.replace(/[-_]([a-z0-9])/g, (_, c: string) =>
c.toUpperCase()); out of the component body and paste it at top-level in the
module, keep the same signature and types, and leave all references inside the
component unchanged so callers continue to use toPluginCamel without other
edits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/components/panels/NodeSettings.tsx`:
- Around line 1949-1954: The code unsafely casts
providerPlugin?.config?.model?.options to {label:string;value:string|number}[]
which breaks when the manifest uses the string[] shorthand and bypasses the
existing normalizeOptions logic; export normalizeOptions from
apps/web/lib/configUtils.ts and in NodeSettings.tsx import and call
normalizeOptions(providerPlugin?.config?.model?.options) to produce a properly
typed options array, then fall back to LLM_MODEL_OPTIONS[dependsOnValue] || []
as before (update the modelOptions assignment to use the normalized result
instead of the direct cast).

---

Outside diff comments:
In `@apps/web/components/panels/NodeSettings.tsx`:
- Line 1890: The bug is that value is set with a fallback to the empty string
(const value = localConfig[field.key] ?? ""), which prevents the later
nullish-coalesce (value ?? field.defaultValue ?? "") from ever using
field.defaultValue; change value so it is undefined when the key is missing
instead of "", e.g. compute value using a presence check like const value =
field.key in localConfig ? localConfig[field.key] : undefined (or use
undefined/null as the fallback) so (value ?? field.defaultValue ?? "") can
correctly fall back to field.defaultValue; update usages referencing
value/localConfig/field.key accordingly.

---

Nitpick comments:
In `@apps/web/components/panels/NodeSettings.tsx`:
- Around line 2454-2456: The helper toPluginCamel is defined inside the
NodeSettings component and is recreated on every render; move this pure function
to module scope (above the component) so it is hoisted and not reallocated each
render. Cut the const toPluginCamel = (id: string) =>
id.replace(/[-_]([a-z0-9])/g, (_, c: string) => c.toUpperCase()); out of the
component body and paste it at top-level in the module, keep the same signature
and types, and leave all references inside the component unchanged so callers
continue to use toPluginCamel without other edits.

- NodeSettings.tsx: value の初期値を undefined に変更し defaultValue フォールバックを有効化
- NodeSettings.tsx: 動的モデルオプションに normalizeOptions を使用(unsafe cast 除去)
- NodeSettings.tsx: toPluginCamel をモジュールスコープに移動
- NodeSettings.tsx: animation-file/model-file の value 型ガード追加
- configUtils.ts: normalizeOptions をエクスポート

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@oboroge0
Copy link
Owner Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/components/panels/NodeSettings.tsx (1)

2055-2067: ⚠️ Potential issue | 🟡 Minor

checkbox doesn't honor field.defaultValue, unlike every other field type.

Line 1894 initializes value to undefined specifically to enable value ?? field.defaultValue ?? "" for text/number/textarea/password. The checkbox case uses checked={!!value}, which treats undefined as false and ignores field.defaultValue. A manifest entry with defaultValue: true for a checkbox will always render unchecked until the user explicitly saves.

🐛 Proposed fix
-              checked={!!value}
+              checked={(value ?? field.defaultValue ?? false) as boolean}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/panels/NodeSettings.tsx` around lines 2055 - 2067, The
checkbox branch in NodeSettings.tsx is ignoring field.defaultValue because it
uses checked={!!value}; update the checkbox to derive its checked state from
value nullish-coalesced with field.defaultValue (e.g., use value ??
field.defaultValue) so a manifest defaultValue: true renders checked initially,
and keep onChange using handleChange(field.key, e.target.checked) to persist
user changes.
🧹 Nitpick comments (2)
apps/web/components/panels/NodeSettings.tsx (2)

1947-1951: field.key.includes("model") is still a heuristic — prefer explicit key matching.

Any future dynamic-select field whose key happens to contain "model" (e.g., "data_model_type", "3d_model_selector") with a dependsOn will silently enter the LLM model branch. The previous review suggested using explicit equality ("model" or "llm_model") or a dedicated optionsSource discriminator; the current includes is an improvement but still imprecise.

♻️ Proposed fix — explicit key allowlist
-        if (field.dynamic && field.dependsOn && field.key.includes("model")) {
+        const LLM_MODEL_KEYS = new Set(["model", "llm_model"]);
+        if (field.dynamic && field.dependsOn && LLM_MODEL_KEYS.has(field.key)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/panels/NodeSettings.tsx` around lines 1947 - 1951, The
branch that detects LLM-model selects uses the fuzzy check
field.key.includes("model"), which can misclassify unrelated fields; update the
condition in the dynamic-select handling (the block that checks field.dynamic
and field.dependsOn) to use an explicit allowlist or discriminator instead — for
example, check field.key === "model" || field.key === "llm_model" or use a
dedicated field.optionsSource === "llm" (or similar) if present; ensure you
update all places referencing this logic (the dynamic model select branch using
providerPluginId and dependsOnValue) so only true LLM model fields take the LLM
path.

2459-2467: getNodeLabel silently ignores its nodeType parameter in the fallback path.

The fallback return schemaLabel (line 2466) uses the outer-scope variable, which is always bound to selectedNode.type, not the nodeType argument. It works today because the only call site is getNodeLabel(selectedNode.type), but the signature implies it's reusable for arbitrary node types.

♻️ Proposed fix — pass schemaLabel as an explicit argument or inline the function
- const getNodeLabel = (nodeType: string): string => {
+ const getNodeLabel = (nodeType: string, fallback: string): string => {
    const derivedKey = `nodeConfig.${toPluginCamel(nodeType)}.label`;
    const derived = t(derivedKey);
    if (derived !== derivedKey) return derived;
-   return schemaLabel;
+   return fallback;
  };

Call site:

- {getNodeLabel(selectedNode.type)}
+ {getNodeLabel(selectedNode.type, schemaLabel)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/panels/NodeSettings.tsx` around lines 2459 - 2467,
getNodeLabel currently ignores its nodeType parameter on the fallback path by
returning the outer-scope schemaLabel (bound to selectedNode.type); fix by
making the fallback depend on the passed nodeType: either add a second parameter
(e.g., schemaLabel) to getNodeLabel and pass the appropriate label from callers
(including where you call getNodeLabel(selectedNode.type)), or compute the
fallback inside getNodeLabel using nodeType (e.g., lookup
nodeConfigs[nodeType]?.label or pluginStore label) so the function no longer
reads the outer selectedNode.type implicitly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/components/panels/NodeSettings.tsx`:
- Around line 1837-1847: The fetch logic only looks at
getPluginById(selectedNode.type)?.config, so built-in UI nodes like
avatar-display that use the static nodeConfigs fallback never trigger
fetchAnimations/fetchModels; update the logic in the component where
getPluginById is used (the block referencing selectedNode.type and pluginConfig)
to also inspect nodeConfigs[selectedNode.type] (or the fallback config object)
for field types, and run fetchAnimations() and fetchModels() when those config
fields include type "animation-file" or "model-file"; keep existing checks for
getPluginById and add the same checks for nodeConfigs to ensure avatar-display
triggers the same fetches.

---

Outside diff comments:
In `@apps/web/components/panels/NodeSettings.tsx`:
- Around line 2055-2067: The checkbox branch in NodeSettings.tsx is ignoring
field.defaultValue because it uses checked={!!value}; update the checkbox to
derive its checked state from value nullish-coalesced with field.defaultValue
(e.g., use value ?? field.defaultValue) so a manifest defaultValue: true renders
checked initially, and keep onChange using handleChange(field.key,
e.target.checked) to persist user changes.

---

Nitpick comments:
In `@apps/web/components/panels/NodeSettings.tsx`:
- Around line 1947-1951: The branch that detects LLM-model selects uses the
fuzzy check field.key.includes("model"), which can misclassify unrelated fields;
update the condition in the dynamic-select handling (the block that checks
field.dynamic and field.dependsOn) to use an explicit allowlist or discriminator
instead — for example, check field.key === "model" || field.key === "llm_model"
or use a dedicated field.optionsSource === "llm" (or similar) if present; ensure
you update all places referencing this logic (the dynamic model select branch
using providerPluginId and dependsOnValue) so only true LLM model fields take
the LLM path.
- Around line 2459-2467: getNodeLabel currently ignores its nodeType parameter
on the fallback path by returning the outer-scope schemaLabel (bound to
selectedNode.type); fix by making the fallback depend on the passed nodeType:
either add a second parameter (e.g., schemaLabel) to getNodeLabel and pass the
appropriate label from callers (including where you call
getNodeLabel(selectedNode.type)), or compute the fallback inside getNodeLabel
using nodeType (e.g., lookup nodeConfigs[nodeType]?.label or pluginStore label)
so the function no longer reads the outer selectedNode.type implicitly.

- NodeSettings.tsx: アニメーション/モデルフェッチで nodeConfigs フォールバックも検査
- NodeSettings.tsx: checkbox で field.defaultValue を反映
- NodeSettings.tsx: 動的モデルセレクトを明示的キー一覧 (model, llm_model) に変更
- NodeSettings.tsx: getNodeLabel の fallback を nodeType パラメータから導出

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@oboroge0
Copy link
Owner Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/components/panels/NodeSettings.tsx (1)

2090-2104: ⚠️ Potential issue | 🟡 Minor

Shared animationInputRef / modelInputRef will misroute uploads when a manifest defines multiple fields of the same type.

Both animation-file and model-file field renderers share a single useRef across all instances. When a node has two animation-file fields (now possible for any community plugin via manifest), React assigns the ref to the last rendered <input type="file">; clicking the first field's "Upload FBX" button silently triggers the second input's onChange, which uses the second field's field.key closure — writing the uploaded URL to the wrong config key.

The cleanest fix without hooks-in-loops is a ref-map pattern:

🐛 Proposed fix — keyed ref map
- const animationInputRef = useRef<HTMLInputElement>(null);
- const modelInputRef = useRef<HTMLInputElement>(null);
+ const animationInputRefs = useRef<Record<string, HTMLInputElement | null>>({});
+ const modelInputRefs    = useRef<Record<string, HTMLInputElement | null>>({});

Inside the animation-file case:

- <input type="file" ref={animationInputRef} ... />
+ <input
+   type="file"
+   ref={(el) => { animationInputRefs.current[field.key] = el; }}
+   ...
+ />
  <button
-   onClick={() => animationInputRef.current?.click()}
+   onClick={() => animationInputRefs.current[field.key]?.click()}
    ...
  >

Apply the same pattern to the model-file case with modelInputRefs.

Also applies to: 2171-2185

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/panels/NodeSettings.tsx` around lines 2090 - 2104, The
file inputs for animation-file and model-file share single refs
(animationInputRef, modelInputRef) causing uploads to be routed to the wrong
field when multiple same-type fields exist; replace each single ref with a keyed
ref map (e.g., animationInputRefs.current: Record<string, HTMLInputElement |
null>) and set the input ref with a callback that assigns
animationInputRefs.current[field.key] = el, update the button onClick to call
animationInputRefs.current[field.key]?.click(), and keep the existing onChange
logic but ensure it reads the file from e.target.files[0] and calls
handleAnimationUpload(file, field.key) (and clears the input value) — apply the
same keyed-ref pattern for modelInputRefs and handleModelUpload in the
model-file renderer.
🧹 Nitpick comments (1)
apps/web/components/panels/NodeSettings.tsx (1)

1864-1868: schemaLabel is dead code — remove it.

schemaLabel is computed but never referenced; the JSX at line 2497 already calls getNodeLabel(selectedNode.type), which covers the same fallback chain (plugin store → nodeConfigs → raw type) plus i18n priority.

♻️ Proposed removal
- const schemaLabel =
-   plugin?.ui?.label ??
-   plugin?.name ??
-   nodeConfigs[selectedNode.type]?.label ??
-   selectedNode.type;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/panels/NodeSettings.tsx` around lines 1864 - 1868, Remove
the dead variable schemaLabel (computed from plugin?.ui?.label, plugin?.name,
nodeConfigs[selectedNode.type]?.label, selectedNode.type) since it is never
used; update NodeSettings.tsx by deleting the const schemaLabel declaration and
any now-unused references to plugin/nodeConfigs solely related to that
computation, and rely on the existing getNodeLabel(selectedNode.type) call
already used in the JSX to provide the label and i18n fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/components/panels/NodeSettings.tsx`:
- Around line 1948-1975: The select branches for dynamic model/VOICEVOX/regular
selects ignore field.defaultValue and use value ?? ""; update each select's
value prop to use the same fallback pattern as other inputs (value ??
field.defaultValue ?? "") so manifest-defined defaults are honored, keeping the
existing onChange handler (handleChange) and option rendering (modelOptions /
VOICEVOX options / normalized options) intact.
- Around line 1951-1958: The code assumes provider plugin models live at
providerPlugin?.config?.model?.options which hardcodes the "model" key; update
NodeSettings.tsx (around providerPlugin, getPluginById, normalizeOptions) to
read the provider-config key dynamically: if the consumer field (when key ===
"llm_model" and dependsOn === "llm_provider") exposes an optionsFromField (or
similar) use that to index providerPlugin.config, otherwise attempt a small
prioritized list of keys such as ["model","llm_model","available_models"] before
falling back to LLM_MODEL_OPTIONS; ensure normalizeOptions is run on the
discovered array and keep the same fallback behavior to LLM_MODEL_OPTIONS.

---

Outside diff comments:
In `@apps/web/components/panels/NodeSettings.tsx`:
- Around line 2090-2104: The file inputs for animation-file and model-file share
single refs (animationInputRef, modelInputRef) causing uploads to be routed to
the wrong field when multiple same-type fields exist; replace each single ref
with a keyed ref map (e.g., animationInputRefs.current: Record<string,
HTMLInputElement | null>) and set the input ref with a callback that assigns
animationInputRefs.current[field.key] = el, update the button onClick to call
animationInputRefs.current[field.key]?.click(), and keep the existing onChange
logic but ensure it reads the file from e.target.files[0] and calls
handleAnimationUpload(file, field.key) (and clears the input value) — apply the
same keyed-ref pattern for modelInputRefs and handleModelUpload in the
model-file renderer.

---

Nitpick comments:
In `@apps/web/components/panels/NodeSettings.tsx`:
- Around line 1864-1868: Remove the dead variable schemaLabel (computed from
plugin?.ui?.label, plugin?.name, nodeConfigs[selectedNode.type]?.label,
selectedNode.type) since it is never used; update NodeSettings.tsx by deleting
the const schemaLabel declaration and any now-unused references to
plugin/nodeConfigs solely related to that computation, and rely on the existing
getNodeLabel(selectedNode.type) call already used in the JSX to provide the
label and i18n fallback.

Comment on lines 1948 to 1975
if (field.dynamic && field.dependsOn && (field.key === "model" || field.key === "llm_model")) {
const dependsOnValue = localConfig[field.dependsOn] as string;
// Try plugin store first: provider "openai" → plugin "openai-llm"
const providerPluginId = `${dependsOnValue}-llm`;
const providerPlugin = getPluginById(providerPluginId);
const dynamicModelOptions = normalizeOptions(providerPlugin?.config?.model?.options);
// Fall back to hardcoded LLM_MODEL_OPTIONS
const modelOptions: { label: string; value: string | number }[] =
dynamicModelOptions && dynamicModelOptions.length > 0
? dynamicModelOptions
: LLM_MODEL_OPTIONS[dependsOnValue] || [];

if (modelOptions.length > 0) {
return (
<select
value={(value ?? "") as string}
onChange={(e) => handleChange(field.key, e.target.value)}
style={inputStyle}
>
<option value="">Select a model...</option>
{modelOptions.map((opt) => (
<option key={String(opt.value)} value={String(opt.value)}>
{opt.label}
</option>
))}
</select>
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

select cases ignore field.defaultValue — inconsistent with all other field types.

text, number, textarea, and checkbox all use value ?? field.defaultValue ?? <empty>, but every select branch (LLM model, VOICEVOX, and regular) uses value ?? "". For manifest-defined fields with a defaultValue on a select, the default is silently dropped and the picker renders empty until the user explicitly chooses.

🐛 Proposed fix — all three select branches
  // LLM model select (line ~1963)
- value={(value ?? "") as string}
+ value={(value ?? field.defaultValue ?? "") as string}

  // VOICEVOX select (line ~2024)
- value={(value ?? "") as string}
+ value={(value ?? field.defaultValue ?? "") as string}

  // Regular select (line ~2043)
- value={(value ?? "") as string}
+ value={(value ?? field.defaultValue ?? "") as string}

Also applies to: 2022-2037, 2041-2053

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/panels/NodeSettings.tsx` around lines 1948 - 1975, The
select branches for dynamic model/VOICEVOX/regular selects ignore
field.defaultValue and use value ?? ""; update each select's value prop to use
the same fallback pattern as other inputs (value ?? field.defaultValue ?? "") so
manifest-defined defaults are honored, keeping the existing onChange handler
(handleChange) and option rendering (modelOptions / VOICEVOX options /
normalized options) intact.

Comment on lines +1951 to +1958
const providerPluginId = `${dependsOnValue}-llm`;
const providerPlugin = getPluginById(providerPluginId);
const dynamicModelOptions = normalizeOptions(providerPlugin?.config?.model?.options);
// Fall back to hardcoded LLM_MODEL_OPTIONS
const modelOptions: { label: string; value: string | number }[] =
dynamicModelOptions && dynamicModelOptions.length > 0
? dynamicModelOptions
: LLM_MODEL_OPTIONS[dependsOnValue] || [];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

providerPlugin?.config?.model hardcodes "model" — community LLM provider plugins must follow this undocumented convention.

When a consumer field has key === "llm_model" and dependsOn === "llm_provider", the dynamic options are still fetched from providerPlugin.config?.model?.options (hardcoded "model"). A community LLM plugin that registers its models under any other config field key (e.g., "llm_model" or "available_models") will silently fall back to LLM_MODEL_OPTIONS, making the manifest-driven intent unreachable.

Consider documenting the "model" field name as a required convention for LLM provider plugins, or generalise the lookup so consumer fields can declare which provider-config key to read from (e.g., an optionsFromField property on ConfigField).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/panels/NodeSettings.tsx` around lines 1951 - 1958, The
code assumes provider plugin models live at
providerPlugin?.config?.model?.options which hardcodes the "model" key; update
NodeSettings.tsx (around providerPlugin, getPluginById, normalizeOptions) to
read the provider-config key dynamically: if the consumer field (when key ===
"llm_model" and dependsOn === "llm_provider") exposes an optionsFromField (or
similar) use that to index providerPlugin.config, otherwise attempt a small
prioritized list of keys such as ["model","llm_model","available_models"] before
falling back to LLM_MODEL_OPTIONS; ensure normalizeOptions is run on the
discovered array and keep the same fallback behavior to LLM_MODEL_OPTIONS.

- NodeSettings.tsx: file input ref を keyed ref map に変更(複数同一型フィールド対応)
- NodeSettings.tsx: 未使用の schemaLabel 変数を削除
- NodeSettings.tsx: 全 select/password の value に field.defaultValue フォールバック追加

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@oboroge0 oboroge0 merged commit 1d07bd7 into main Feb 19, 2026
4 checks passed
oboroge0 added a commit that referenced this pull request Feb 19, 2026
- プラグイン設定の動的レンダリング化 (#93)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@oboroge0 oboroge0 deleted the refactor/dynamic-plugin-settings branch February 19, 2026 11:53
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