@xingjisoft/neutrino 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +6 -1
  2. package/assets/fast-planner.txt +57 -0
  3. package/assets/fast-replanner.txt +28 -0
  4. package/dist/agents.d.ts +25 -0
  5. package/dist/app.d.ts +206 -0
  6. package/dist/app.js +15 -0
  7. package/dist/background.d.ts +96 -0
  8. package/dist/bin.d.ts +2 -0
  9. package/dist/bin.js +40 -3
  10. package/dist/browser.d.ts +18 -0
  11. package/dist/clipboard.d.ts +16 -0
  12. package/dist/colors.d.ts +29 -0
  13. package/dist/compact.d.ts +52 -0
  14. package/dist/config.d.ts +221 -0
  15. package/dist/config.js +22 -7
  16. package/dist/cwd.d.ts +19 -0
  17. package/dist/fast/artifacts.d.ts +26 -0
  18. package/dist/fast/artifacts.js +109 -0
  19. package/dist/fast/engine.d.ts +78 -0
  20. package/dist/fast/engine.js +657 -0
  21. package/dist/fast/evidence.d.ts +21 -0
  22. package/dist/fast/evidence.js +68 -0
  23. package/dist/fast/planner.d.ts +10 -0
  24. package/dist/fast/planner.js +51 -0
  25. package/dist/fast/pool.d.ts +17 -0
  26. package/dist/fast/pool.js +78 -0
  27. package/dist/fast/scheduler.d.ts +54 -0
  28. package/dist/fast/scheduler.js +383 -0
  29. package/dist/fast/types.d.ts +100 -0
  30. package/dist/fast/types.js +21 -0
  31. package/dist/fast/worker.d.ts +23 -0
  32. package/dist/fast/worker.js +185 -0
  33. package/dist/header.d.ts +3 -0
  34. package/dist/index.d.ts +5 -0
  35. package/dist/llm/approval.d.ts +204 -0
  36. package/dist/llm/approval.js +14 -3
  37. package/dist/llm/client.d.ts +118 -0
  38. package/dist/llm/client.js +57 -8
  39. package/dist/llm/codeExecutor.d.ts +14 -0
  40. package/dist/llm/codemode.d.ts +15 -0
  41. package/dist/llm/context.d.ts +29 -0
  42. package/dist/llm/context.js +17 -4
  43. package/dist/llm/diagnostics.d.ts +91 -0
  44. package/dist/llm/diagnostics.js +242 -0
  45. package/dist/llm/plan.d.ts +148 -0
  46. package/dist/llm/retry.d.ts +80 -0
  47. package/dist/llm/risk.d.ts +21 -0
  48. package/dist/llm/risk.js +7 -0
  49. package/dist/llm/runtime.d.ts +189 -0
  50. package/dist/llm/runtime.js +178 -24
  51. package/dist/llm/search.d.ts +2 -0
  52. package/dist/llm/searchNetwork.d.ts +8 -0
  53. package/dist/llm/subagent.d.ts +112 -0
  54. package/dist/llm/subagent.js +25 -12
  55. package/dist/llm/tools.d.ts +20 -0
  56. package/dist/llm/tools.js +4 -1
  57. package/dist/llm/workspace.d.ts +6 -0
  58. package/dist/llm/workspace.js +271 -0
  59. package/dist/markdown.d.ts +58 -0
  60. package/dist/migration.d.ts +8 -0
  61. package/dist/persistence.d.ts +14 -0
  62. package/dist/plugins/catalog.d.ts +20 -0
  63. package/dist/plugins/catalog.js +45 -0
  64. package/dist/plugins/cli.d.ts +1 -0
  65. package/dist/plugins/cli.js +106 -0
  66. package/dist/plugins/host.d.ts +121 -0
  67. package/dist/plugins/host.js +450 -0
  68. package/dist/plugins/manager.d.ts +24 -0
  69. package/dist/plugins/manager.js +288 -0
  70. package/dist/plugins/package.d.ts +5 -0
  71. package/dist/plugins/package.js +53 -0
  72. package/dist/plugins/runner.d.ts +1 -0
  73. package/dist/plugins/runner.js +227 -0
  74. package/dist/plugins/session.d.ts +12 -0
  75. package/dist/plugins/session.js +97 -0
  76. package/dist/plugins/terminal.d.ts +22 -0
  77. package/dist/plugins/terminal.js +33 -0
  78. package/dist/plugins/types.d.ts +178 -0
  79. package/dist/plugins/types.js +6 -0
  80. package/dist/plugins/validation.d.ts +14 -0
  81. package/dist/plugins/validation.js +174 -0
  82. package/dist/plugins/web-sdk.d.ts +58 -0
  83. package/dist/plugins/web-sdk.js +1 -0
  84. package/dist/pro/agents.d.ts +34 -0
  85. package/dist/pro/agents.js +1 -1
  86. package/dist/pro/config.d.ts +75 -0
  87. package/dist/pro/config.js +3 -2
  88. package/dist/pro/preflight.d.ts +21 -0
  89. package/dist/sanitize.d.ts +34 -0
  90. package/dist/screens/agentConversation.d.ts +28 -0
  91. package/dist/screens/agentHistory.d.ts +25 -0
  92. package/dist/screens/agentSelect.d.ts +16 -0
  93. package/dist/screens/chat.d.ts +292 -0
  94. package/dist/screens/chat.js +179 -65
  95. package/dist/screens/history.d.ts +23 -0
  96. package/dist/screens/modelSelect.d.ts +29 -0
  97. package/dist/screens/plugins.d.ts +29 -0
  98. package/dist/screens/plugins.js +196 -0
  99. package/dist/screens/settings.d.ts +42 -0
  100. package/dist/screens/start.d.ts +43 -0
  101. package/dist/screens/start.js +25 -17
  102. package/dist/screens/updatePrompt.d.ts +41 -0
  103. package/dist/selection.d.ts +75 -0
  104. package/dist/state.d.ts +26 -0
  105. package/dist/terminal/buffer.d.ts +47 -0
  106. package/dist/terminal/input.d.ts +70 -0
  107. package/dist/terminal/renderer.d.ts +43 -0
  108. package/dist/terminal/restore.d.ts +41 -0
  109. package/dist/terminal/screen.d.ts +57 -0
  110. package/dist/tray.d.ts +14 -0
  111. package/dist/ui.d.ts +123 -0
  112. package/dist/update.d.ts +71 -0
  113. package/dist/updateWatch.d.ts +43 -0
  114. package/dist/web/approvalQueue.d.ts +8 -0
  115. package/dist/web/approvalQueue.js +59 -0
  116. package/dist/web/auth.d.ts +14 -0
  117. package/dist/web/clientState.d.ts +16 -0
  118. package/dist/web/folderPicker.d.ts +34 -0
  119. package/dist/web/folderPicker.js +544 -0
  120. package/dist/web/hub.d.ts +213 -0
  121. package/dist/web/hub.js +14 -2
  122. package/dist/web/localApi.d.ts +11 -0
  123. package/dist/web/localApi.js +260 -0
  124. package/dist/web/plugins.d.ts +8 -0
  125. package/dist/web/plugins.js +64 -0
  126. package/dist/web/server.d.ts +81 -0
  127. package/dist/web/server.js +127 -19
  128. package/dist/web/service.d.ts +7 -0
  129. package/dist/web/service.js +1 -0
  130. package/dist/web/session.d.ts +176 -0
  131. package/dist/web/session.js +170 -21
  132. package/dist/web/settings.d.ts +65 -0
  133. package/dist/web/terminalBridge.d.ts +15 -0
  134. package/dist/web/terminalBridge.js +8 -2
  135. package/dist/width.d.ts +6 -0
  136. package/docs/plugins/README.md +126 -0
  137. package/docs/plugins/agent-sdk.md +92 -0
  138. package/docs/plugins/architecture.md +64 -0
  139. package/docs/plugins/compatibility.md +34 -0
  140. package/docs/plugins/development.md +55 -0
  141. package/docs/plugins/manifest.md +59 -0
  142. package/docs/plugins/manifest.schema.json +286 -0
  143. package/docs/plugins/security.md +53 -0
  144. package/docs/plugins/skills-prompts.md +69 -0
  145. package/docs/plugins/ui-sdk.md +109 -0
  146. package/examples/local-api.py +105 -0
  147. package/examples/plugins/cards/card.html +6 -0
  148. package/examples/plugins/cards/neutrino.plugin.json +42 -0
  149. package/examples/plugins/composer/editor.html +10 -0
  150. package/examples/plugins/composer/neutrino.plugin.json +10 -0
  151. package/examples/plugins/function-tools/agent.js +32 -0
  152. package/examples/plugins/function-tools/neutrino.plugin.json +15 -0
  153. package/examples/plugins/menus-tabs/neutrino.plugin.json +15 -0
  154. package/examples/plugins/menus-tabs/panel.html +6 -0
  155. package/examples/plugins/skills-prompts/neutrino.plugin.json +13 -0
  156. package/examples/plugins/skills-prompts/prompts/evidence.md +1 -0
  157. package/examples/plugins/skills-prompts/prompts/reviewer.md +1 -0
  158. package/examples/plugins/skills-prompts/skills/review/SKILL.md +8 -0
  159. package/examples/plugins/theme/neutrino.plugin.json +12 -0
  160. package/package.json +66 -42
  161. package/web/LOCAL-API.md +65 -0
  162. package/web/README.md +30 -8
  163. package/web/app.js +256 -67
  164. package/web/i18n.js +18 -3
  165. package/web/index.html +3 -1
  166. package/web/plugins.css +10 -0
  167. package/web/plugins.js +230 -0
  168. package/web/style.css +12 -3
  169. package/web/turns.js +7 -8
package/web/app.js CHANGED
@@ -1,7 +1,9 @@
1
+ import { createPluginUI } from "./plugins.js";
2
+ let pluginUI = null;
1
3
  import { createSettingsUI } from "./settings.js";
2
4
  import { createProUI } from "./pro.js";
3
5
  import { renderMarkdown as renderMarkdownHtml } from "./markdown.js";
4
- import { turnAnswerIndex, turnFolds, turnPreview, turnRanges } from "./turns.js";
6
+ import { turnAnswerIndex, turnFolds, turnRanges } from "./turns.js";
5
7
  import { applyTranslations, getLanguage as currentLanguage, locale as currentLocale, setLanguage as setI18nLanguage, t as translate } from "./i18n.js";
6
8
 
7
9
  // Neutrino web UI — a dependency-free single page app.
@@ -596,9 +598,9 @@ function storedWorkspace() {
596
598
  return { key: pathKey(path), path };
597
599
  }
598
600
 
599
- const AGENT_MODE_LABELS = { standard: "Standard mode", ptc: "PTC mode", fast: "Fast mode", pro: "Professional mode" };
601
+ const AGENT_MODE_LABELS = { standard: "Standard mode", ptc: "PTC mode", fast: "Fast mode", fast_dag: "Fast Mode", pro: "Professional mode" };
600
602
  let newAgentMode = readStorage("neutrino.agent-mode");
601
- if (!["standard", "ptc", "fast"].includes(newAgentMode)) newAgentMode = "standard";
603
+ if (!["standard", "ptc", "fast", "fast_dag"].includes(newAgentMode)) newAgentMode = "standard";
602
604
  let agentModePending = Promise.resolve();
603
605
  let agentModeChanging = false;
604
606
  let workspaceNavigation = false;
@@ -625,7 +627,8 @@ function renderAgentMode() {
625
627
  el.modePicker.classList.toggle("is-pending", Boolean(agentModeChanging || nextTurn));
626
628
  for (const button of el.modePicker.querySelectorAll?.("[data-agent-mode]") || []) {
627
629
  button.setAttribute("aria-checked", String(button.dataset.agentMode === mode));
628
- button.disabled = agentModeChanging || workspaceNavigation;
630
+ button.disabled = agentModeChanging || workspaceNavigation || (button.dataset.agentMode === "fast_dag" && state.overview?.fast_mode_enabled === false);
631
+ if (button.dataset.agentMode === "fast_dag") button.title = state.overview?.fast_mode_enabled === false ? tr("Fast Mode is disabled; select another mode.") : "";
629
632
  }
630
633
  if (el.modeFeedback) {
631
634
  el.modeFeedback.textContent = agentModeError || (agentModeChanging ? tr("Changing mode…") : nextTurn ? tr("Applies to the next turn") : "");
@@ -727,7 +730,7 @@ function initializeAgentMode() {
727
730
  if (event.key === "Escape") { el.modePicker.open = false; el.modePicker.querySelector("summary")?.focus(); event.stopPropagation(); }
728
731
  if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
729
732
  event.preventDefault(); el.modePicker.open = true;
730
- const buttons = [...el.modePicker.querySelectorAll("[data-agent-mode]")];
733
+ const buttons = [...el.modePicker.querySelectorAll("[data-agent-mode]")].filter(button => !button.disabled);
731
734
  let index = buttons.indexOf(document.activeElement);
732
735
  index = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + buttons.length) % buttons.length;
733
736
  buttons[index]?.focus();
@@ -983,6 +986,7 @@ async function loadOverview(options = {}) {
983
986
  const overview = await response.json();
984
987
  if (gateShown) return;
985
988
  state.overview = overview;
989
+ if (el.modePicker) renderAgentMode();
986
990
  setStatus(state.selection?.kind === "live" ? "live" : "online", state.selection?.kind === "live" ? "live" : "online");
987
991
  renderWorkspaceChrome();
988
992
  renderSidebar();
@@ -1628,14 +1632,32 @@ function canPauseFromComposer() {
1628
1632
  && snapshot.busy && !snapshot.paused && !el.input.value.trim());
1629
1633
  }
1630
1634
 
1635
+ // A paused turn resumes from the composer button, exactly like an empty draft
1636
+ // pauses a running one: the draft wins, so typing always means "send".
1637
+ function canResumeFromComposer() {
1638
+ const selection = state.selection;
1639
+ const snapshot = state.snapshot;
1640
+ return Boolean(selection?.kind === "live" && snapshot?.key === selection.key
1641
+ && snapshot.paused && !el.input.value.trim());
1642
+ }
1643
+
1644
+ function composerAction() {
1645
+ if (canPauseFromComposer()) return "pause";
1646
+ if (canResumeFromComposer()) return "resume";
1647
+ return "send";
1648
+ }
1649
+
1631
1650
  function renderComposerAction() {
1632
- const pause = canPauseFromComposer();
1633
- const label = pause ? "Pause" : "Send message";
1634
- el.send.classList.toggle("is-pause", pause);
1635
- el.send.setAttribute("data-i18n-title", label);
1636
- el.send.setAttribute("data-i18n-aria-label", label);
1637
- el.send.title = tr(label);
1638
- el.send.setAttribute("aria-label", tr(label));
1651
+ const action = composerAction();
1652
+ el.send.classList.toggle("is-pause", action === "pause");
1653
+ el.send.classList.toggle("is-resume", action === "resume");
1654
+ const [source, text] = action === "pause" ? ["Pause", tr("Pause")]
1655
+ : action === "resume" ? ["Resume", tr("Resume")]
1656
+ : ["Send message", tr("Send message")];
1657
+ el.send.setAttribute("data-i18n-title", source);
1658
+ el.send.setAttribute("data-i18n-aria-label", source);
1659
+ el.send.title = text;
1660
+ el.send.setAttribute("aria-label", text);
1639
1661
  }
1640
1662
 
1641
1663
  function hideComposer() {
@@ -1885,7 +1907,7 @@ function renderControls(snapshot) {
1885
1907
  ? reviewStatus(reviewing)
1886
1908
  : subagent
1887
1909
  ? tr("sub-agent: {label}", { label: subagent })
1888
- : tr("working")
1910
+ : snapshot.fast && snapshot.activeAgentMode === "fast_dag" ? fastPhaseLabel(snapshot.fast.phase) : tr("working")
1889
1911
  : tr("idle");
1890
1912
  el.controls.appendChild(badge);
1891
1913
 
@@ -1896,12 +1918,15 @@ function renderControls(snapshot) {
1896
1918
  el.controls.appendChild(warning);
1897
1919
  }
1898
1920
 
1899
- if (snapshot.busy || snapshot.paused) {
1921
+ // Resuming belongs to the composer button (see renderComposerAction), which
1922
+ // offers "Continue" on an empty draft, so the status bar never shows a second
1923
+ // way in. Pausing stays here, for a turn that is actually running.
1924
+ if (snapshot.busy && !snapshot.paused) {
1900
1925
  const button = document.createElement("button");
1901
1926
  button.className = "control";
1902
- button.textContent = snapshot.paused ? tr("Resume") : tr("Pause");
1927
+ button.textContent = tr("Pause");
1903
1928
  button.addEventListener("click", () => {
1904
- postAction(snapshot.key, snapshot.paused ? "resume" : "pause");
1929
+ postAction(snapshot.key, "pause");
1905
1930
  });
1906
1931
  el.controls.appendChild(button);
1907
1932
  }
@@ -1953,30 +1978,48 @@ function renderMessageList(messages, streaming, options) {
1953
1978
  context.end <= original.length && typeof context.summary === "string" && context.summary.trim()
1954
1979
  ? context.end : 0;
1955
1980
  if (end) {
1956
- const details = document.createElement("details");
1957
- details.className = "conversation-history";
1958
- const summary = document.createElement("summary");
1959
- summary.textContent = typeof tr === "function"
1960
- ? tr("Earlier conversation · {n} messages", { n: end })
1961
- : `Earlier conversation · ${end} messages`;
1962
- details.appendChild(summary);
1963
- const body = document.createElement("div");
1964
- body.className = "history-messages";
1965
- details.appendChild(body);
1966
- const populate = () => {
1967
- if (body.childNodes.length) return;
1968
- original.slice(0, end).forEach((message, index) => body.appendChild(renderMessage(message, index)));
1969
- };
1970
- details.open = state.openContext === end;
1971
- if (details.open) populate();
1972
- details.addEventListener("toggle", () => {
1973
- // Detached disclosures can emit a delayed toggle during a stream repaint.
1974
- if (!details.isConnected) return;
1975
- state.openContext = details.open ? end : null;
1976
- if (details.open) populate();
1977
- else body.replaceChildren();
1978
- });
1979
- el.messages.appendChild(details);
1981
+ // Keep user input in place and fold only the AI output between prompts.
1982
+ // The disclosures share one expansion state for this compressed prefix.
1983
+ const sections = [];
1984
+ for (let index = 0; index < end;) {
1985
+ if (original[index].role === "user") {
1986
+ el.messages.appendChild(renderMessage(original[index], index));
1987
+ index++;
1988
+ continue;
1989
+ }
1990
+ const start = index;
1991
+ while (index < end && original[index].role !== "user") index++;
1992
+ const archived = original.slice(start, index);
1993
+ const details = document.createElement("details");
1994
+ details.className = "conversation-history";
1995
+ const summary = document.createElement("summary");
1996
+ summary.textContent = typeof tr === "function"
1997
+ ? tr("Earlier conversation · {n} messages", { n: archived.length })
1998
+ : `Earlier conversation · ${archived.length} messages`;
1999
+ details.appendChild(summary);
2000
+ const body = document.createElement("div");
2001
+ body.className = "history-messages";
2002
+ details.appendChild(body);
2003
+ const populate = () => {
2004
+ if (body.childNodes.length) return;
2005
+ archived.forEach((message, offset) => body.appendChild(renderMessage(message, start + offset)));
2006
+ };
2007
+ const sync = (open) => {
2008
+ if (details.open !== open) details.open = open;
2009
+ if (open) populate();
2010
+ else body.replaceChildren();
2011
+ };
2012
+ sections.push(sync);
2013
+ sync(state.openContext === end);
2014
+ details.addEventListener("toggle", () => {
2015
+ // Detached disclosures can emit a delayed toggle during a stream repaint.
2016
+ if (!details.isConnected) return;
2017
+ const open = details.open;
2018
+ state.openContext = open ? end : null;
2019
+ for (const sync of sections) sync(open);
2020
+ });
2021
+ el.messages.appendChild(details);
2022
+ }
1980
2023
  }
1981
2024
  // Render the tail one turn at a time: a finished turn folds its work behind a
1982
2025
  // summary line and keeps its reply; a running one stays fully open. The turn in
@@ -2010,6 +2053,7 @@ function renderMessageList(messages, streaming, options) {
2010
2053
  function renderMessage(message, index) {
2011
2054
  const article = document.createElement("article");
2012
2055
  article.className = `message ${message.role}`;
2056
+ article.dataset.pluginMessage = String(index);
2013
2057
 
2014
2058
  if (message.role === "tool") {
2015
2059
  const details = document.createElement("details");
@@ -2058,8 +2102,8 @@ function renderMessage(message, index) {
2058
2102
  }
2059
2103
 
2060
2104
  /**
2061
- * A finished turn: its work folded behind one control line, its reply always on
2062
- * screen below it. The folded messages are rendered on first open and dropped
2105
+ * A finished turn: its complete user input above the fold, its reply below it.
2106
+ * The folded AI messages are rendered on first open and dropped
2063
2107
  * again on close, so a long transcript costs nothing until the reader asks for
2064
2108
  * it. Nothing is lost either way: this is presentation only, and the saved
2065
2109
  * transcript always holds every message.
@@ -2071,25 +2115,27 @@ function renderTurnFold(start, messages) {
2071
2115
  ? tr(text, values)
2072
2116
  : String(text).replace(/\{(\w+)\}/g, (match, name) =>
2073
2117
  values && Object.prototype.hasOwnProperty.call(values, name) ? String(values[name]) : match));
2074
- // The reply is never folded away, so a turn renders as a group: the fold (the
2075
- // prompt line plus the work it hides) and, right below it, the answer itself.
2118
+ // User input and the reply stay outside the disclosure, in transcript order.
2076
2119
  const reply = turnAnswerIndex(messages);
2077
2120
  const group = document.createElement("div");
2078
2121
  group.className = "turn-group";
2122
+ messages.forEach((message, offset) => {
2123
+ if (message.role === "user") group.appendChild(renderMessage(message, start + offset));
2124
+ });
2079
2125
 
2080
2126
  const details = document.createElement("details");
2081
2127
  details.className = "turn";
2082
2128
  details.open = state.openTurns.has(start);
2083
2129
 
2084
- const hidden = reply < 0 ? messages.length : messages.length - 1;
2130
+ const hidden = messages.filter((message, offset) => message.role !== "user" && offset !== reply).length;
2085
2131
  const summary = document.createElement("summary");
2086
2132
  const chevron = document.createElement("span");
2087
2133
  chevron.className = "turn-chevron";
2088
2134
  chevron.innerHTML =
2089
2135
  '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>';
2090
- const you = document.createElement("span");
2091
- you.className = "turn-you";
2092
- you.textContent = tx("You: {text}", { text: truncate(turnPreview(messages, "user"), 110) });
2136
+ const label = document.createElement("span");
2137
+ label.className = "turn-label";
2138
+ label.textContent = "Neutrino";
2093
2139
  const meta = document.createElement("span");
2094
2140
  meta.className = "turn-meta";
2095
2141
  meta.textContent = tx("{n} of {m} messages folded", { n: hidden, m: messages.length });
@@ -2098,7 +2144,7 @@ function renderTurnFold(start, messages) {
2098
2144
  hint.className = "turn-hint";
2099
2145
  meta.appendChild(hint);
2100
2146
  summary.appendChild(chevron);
2101
- summary.appendChild(you);
2147
+ summary.appendChild(label);
2102
2148
  summary.appendChild(meta);
2103
2149
  details.appendChild(summary);
2104
2150
  group.appendChild(details);
@@ -2114,7 +2160,7 @@ function renderTurnFold(start, messages) {
2114
2160
  const populate = () => {
2115
2161
  if (body.childNodes.length) return;
2116
2162
  messages.forEach((message, offset) => {
2117
- if (offset === reply) return; // the answer is never inside the fold
2163
+ if (message.role === "user" || offset === reply) return;
2118
2164
  body.appendChild(renderMessage(message, start + offset));
2119
2165
  });
2120
2166
  };
@@ -2158,52 +2204,69 @@ function scrollToBottom() {
2158
2204
 
2159
2205
  async function postAction(key, action, body) {
2160
2206
  try {
2161
- await apiFetch(`/api/live/${encodeURIComponent(key)}/${action}`, {
2207
+ const response = await apiFetch(`/api/live/${encodeURIComponent(key)}/${action}`, {
2162
2208
  method: "POST",
2163
2209
  headers: body ? { "Content-Type": "application/json" } : undefined,
2164
2210
  body: body ? JSON.stringify(body) : undefined,
2165
2211
  });
2212
+ if (!response.ok) { const data = await response.json(); throw new Error(data.error || `HTTP ${response.status}`); }
2213
+ return true;
2166
2214
  } catch (error) {
2215
+ el.sub.textContent = error.message;
2167
2216
  console.error(error);
2217
+ return false;
2168
2218
  }
2169
2219
  }
2170
2220
 
2171
2221
  function activateComposer() {
2172
- if (canPauseFromComposer()) return postAction(state.selection.key, "pause");
2222
+ const action = composerAction();
2223
+ if (action === "pause" || action === "resume") return postAction(state.selection.key, action);
2173
2224
  return sendMessage();
2174
2225
  }
2175
2226
 
2176
2227
  async function sendMessage() {
2228
+ // Internal plugin requests pin their originating selection and can preserve the user draft.
2229
+ const pluginRequest = arguments[0] ?? null;
2230
+ // The keydown handler passes the text it saw: sendMessage awaits the agent-mode
2231
+ // preference before touching the textarea, so reading it here is too late.
2232
+ const submittedText = arguments[1] ?? null;
2177
2233
  try { await agentModePending; } catch (error) { el.sub.textContent = error.message; return; }
2234
+ if (pluginRequest && state.selection !== pluginRequest.selection) throw new Error("Conversation changed before the plugin message was sent");
2178
2235
  const selection = state.selection;
2179
2236
  if (!selection) return;
2180
- const text = el.input.value;
2237
+ const text = pluginRequest?.text ?? submittedText ?? el.input.value;
2181
2238
  if (!text.trim()) return;
2182
2239
  try {
2183
2240
  if (PRO_MODE) await proUI?.flush();
2184
2241
  else await preferences.flush?.();
2185
2242
  } catch (error) { el.sub.textContent = tr(error.message); return; }
2186
- if (state.selection !== selection) return;
2243
+ if (state.selection !== selection) { if (pluginRequest) throw new Error("Conversation changed before the plugin message was sent"); return; }
2244
+ const messagePayload = { text, ...(PRO_MODE ? {} : { agent_mode: currentAgentMode() }) };
2187
2245
 
2188
2246
  if (selection.kind === "live") {
2189
- el.input.value = "";
2247
+ if (!pluginRequest?.preserveDraft) el.input.value = "";
2190
2248
  autosize();
2191
- await postAction(selection.key, "message", { text });
2249
+ // A paused turn only queues the text, so a user send resumes right away: the
2250
+ // message then starts the turn instead of waiting for a separate continue click.
2251
+ const resumes = !pluginRequest && state.snapshot?.key === selection.key && Boolean(state.snapshot.paused);
2252
+ const sent = await postAction(selection.key, "message", messagePayload);
2253
+ if (sent === false) { if (!pluginRequest?.preserveDraft) el.input.value = text; autosize(); }
2254
+ else if (resumes) await postAction(selection.key, "resume");
2192
2255
  return;
2193
2256
  }
2194
2257
 
2195
2258
  if (selection.kind === "new") {
2196
2259
  // A workspace landing page: typing starts a fresh conversation there, just
2197
2260
  // like typing on the terminal's start screen.
2198
- el.input.value = "";
2261
+ if (!pluginRequest?.preserveDraft) el.input.value = "";
2199
2262
  autosize();
2200
2263
  try {
2201
2264
  const created = await createConversation(selection.cwd, preferences.defaultSelection?.());
2202
2265
  await selectLive(created.key);
2203
2266
  setConversationView(true);
2204
- await postAction(created.key, "message", { text });
2267
+ if (await postAction(created.key, "message", messagePayload) === false) { if (!pluginRequest?.preserveDraft) el.input.value = text; autosize(); }
2205
2268
  } catch (error) {
2206
- el.input.value = text;
2269
+ if (!pluginRequest?.preserveDraft) el.input.value = text;
2207
2270
  autosize();
2208
2271
  el.sub.textContent = tr("Could not start the conversation: {error}", { error: error.message });
2209
2272
  }
@@ -2211,13 +2274,13 @@ async function sendMessage() {
2211
2274
  }
2212
2275
 
2213
2276
  // Reading a saved transcript: resume it in the browser, then deliver the text.
2214
- el.input.value = "";
2277
+ if (!pluginRequest?.preserveDraft) el.input.value = "";
2215
2278
  autosize();
2216
2279
  try {
2217
2280
  const key = await continueSession(selection.id);
2218
- await postAction(key, "message", { text });
2281
+ if (await postAction(key, "message", messagePayload) === false) { if (!pluginRequest?.preserveDraft) el.input.value = text; autosize(); }
2219
2282
  } catch (error) {
2220
- el.input.value = text;
2283
+ if (!pluginRequest?.preserveDraft) el.input.value = text;
2221
2284
  autosize();
2222
2285
  el.sub.textContent = tr("Could not continue this conversation: {error}", { error: error.message });
2223
2286
  }
@@ -2689,6 +2752,46 @@ async function loadFolder(path) {
2689
2752
  }
2690
2753
  }
2691
2754
 
2755
+ /**
2756
+ * Ask the server to open the operating system's folder window.
2757
+ *
2758
+ * The browser's own picker never reveals an absolute path, so a working
2759
+ * directory has to be chosen on the machine that runs the agent. Resolves to the
2760
+ * chosen path, "" when the user closed the window, or null when this browser
2761
+ * cannot use a system window: another device, or no desktop behind the server.
2762
+ */
2763
+ async function pickFolderInSystemWindow(start, feedback) {
2764
+ if (feedback) feedback.textContent = tr("Waiting for the folder window on your desktop…");
2765
+ try {
2766
+ const response = await apiFetch("/api/fs/pick", {
2767
+ method: "POST",
2768
+ headers: { "Content-Type": "application/json" },
2769
+ body: JSON.stringify({ start, title: tr("Select a working directory") }),
2770
+ });
2771
+ const data = await response.json().catch(() => ({}));
2772
+ if (!response.ok) return null;
2773
+ return typeof data.path === "string" ? data.path : "";
2774
+ } catch {
2775
+ // A window that could not open is a reason to fall back, never an error.
2776
+ return null;
2777
+ } finally {
2778
+ if (feedback) feedback.textContent = "";
2779
+ }
2780
+ }
2781
+
2782
+ /**
2783
+ * Choose a working directory: the system folder window first, the in-page
2784
+ * folder browser when no window can open here.
2785
+ */
2786
+ async function chooseFolder({ start = "", onPick = null, feedback = null } = {}) {
2787
+ const picked = await pickFolderInSystemWindow(start, feedback);
2788
+ if (picked === null) {
2789
+ openFolderDialog({ start, onPick });
2790
+ return;
2791
+ }
2792
+ if (picked && typeof onPick === "function") onPick(picked);
2793
+ }
2794
+
2692
2795
  function openFolderDialog({ start = "", onPick = null } = {}) {
2693
2796
  state.folder = {
2694
2797
  path: "",
@@ -2792,12 +2895,28 @@ function planStatusLabel(status) {
2792
2895
  return tr("Pending");
2793
2896
  }
2794
2897
 
2898
+ function fastPhaseLabel(phase) {
2899
+ switch (phase) {
2900
+ case "planning": return tr("Planning tasks");
2901
+ case "executing": return tr("Running tasks concurrently");
2902
+ case "verifying": return tr("Verifying local results");
2903
+ case "integrating": return tr("Integrating results");
2904
+ case "reviewing": return tr("Final global review");
2905
+ case "completed": return tr("Fast Mode completed");
2906
+ case "cancelled": return tr("Fast Mode cancelled");
2907
+ case "cancelling": return tr("Stopping tasks…");
2908
+ default: return tr("Fast Mode failed");
2909
+ }
2910
+ }
2911
+
2795
2912
  function renderPlanCard(source) {
2796
2913
  if (!el.planCard) return;
2797
2914
  const plan = source && source.plan && Array.isArray(source.plan.steps) && source.plan.steps.length ? source.plan : null;
2798
2915
  const tokens = source && source.tokens ? source.tokens : null;
2799
2916
  const hasUsage = tokens && (Number(tokens.prompt) > 0 || Number(tokens.completion) > 0);
2800
- if (!state.planOpen || (!plan && !hasUsage)) {
2917
+ const fast = (source?.activeAgentMode === "fast_dag" || (!source?.activeAgentMode && source?.agent_mode === "fast_dag"))
2918
+ ? source.fast || source.fast_run : null;
2919
+ if (!state.planOpen || (!plan && !hasUsage && !fast)) {
2801
2920
  el.planCard.classList.add("hidden");
2802
2921
  el.planCard.replaceChildren();
2803
2922
  return;
@@ -2822,6 +2941,39 @@ function renderPlanCard(source) {
2822
2941
  head.append(title, close);
2823
2942
  card.appendChild(head);
2824
2943
 
2944
+ if (fast) {
2945
+ const progress = document.createElement("div");
2946
+ progress.className = "fast-progress";
2947
+ progress.setAttribute("role", "status");
2948
+ progress.setAttribute("aria-live", "polite");
2949
+ const phase = document.createElement("strong");
2950
+ phase.textContent = tr("Fast Mode") + " · " + fastPhaseLabel(fast.phase);
2951
+ const tasks = (fast.tasks || []).filter(task => !task.verifies);
2952
+ const count = document.createElement("p");
2953
+ count.textContent = tr("{running} running · {verified}/{total} verified", {
2954
+ running: (fast.tasks || []).filter(task => task.status === "RUNNING" || (task.kind === "verification" && task.status === "VERIFYING")).length,
2955
+ verified: tasks.filter(task => task.status === "COMPLETED").length, total: tasks.length,
2956
+ });
2957
+ progress.append(phase, count);
2958
+ const list = document.createElement("ul");
2959
+ for (const task of tasks) {
2960
+ const row = document.createElement("li");
2961
+ row.textContent = `${task.task_goal} · ${task.status}`;
2962
+ list.appendChild(row);
2963
+ }
2964
+ progress.appendChild(list);
2965
+ if (fast.error) {
2966
+ const error = document.createElement("p");
2967
+ error.className = "error"; error.textContent = fast.error;
2968
+ const fallback = document.createElement("button");
2969
+ fallback.type = "button"; fallback.className = "control";
2970
+ fallback.textContent = tr("Select Standard mode to continue");
2971
+ fallback.addEventListener("click", () => { agentModePending = chooseAgentMode("standard"); void agentModePending.catch(() => {}); });
2972
+ progress.append(error, fallback);
2973
+ }
2974
+ card.appendChild(progress);
2975
+ }
2976
+
2825
2977
  if (plan) {
2826
2978
  if (plan.goal) {
2827
2979
  const goal = document.createElement("div");
@@ -3347,9 +3499,12 @@ el.back.addEventListener("click", () => setConversationView(false));
3347
3499
  el.send.addEventListener("click", activateComposer);
3348
3500
  el.input.addEventListener("input", autosize);
3349
3501
  el.input.addEventListener("keydown", (event) => {
3502
+ // An Enter that picks an IME candidate belongs to the input method, not to us:
3503
+ // preventDefault would cancel the commit and send the uncommitted composition.
3504
+ if (event.isComposing || event.keyCode === 229) return;
3350
3505
  if (event.key === "Enter" && !event.shiftKey) {
3351
3506
  event.preventDefault();
3352
- sendMessage();
3507
+ sendMessage(null, el.input.value);
3353
3508
  }
3354
3509
  });
3355
3510
 
@@ -3363,8 +3518,20 @@ el.workspaceAll.addEventListener("click", () => {
3363
3518
  showHome();
3364
3519
  });
3365
3520
  el.workspaceAdd.addEventListener("click", () => {
3366
- el.workspaceDialog.close();
3367
- openFolderDialog({ start: state.workspace?.path || state.workspaces?.cwd || "", onPick: (path) => openWorkspace(path) });
3521
+ // The window opens on the desktop of the machine running the console, so this
3522
+ // sheet stays open with a line saying where to look, and closes once a folder
3523
+ // has been chosen (or when the in-page browser takes over).
3524
+ const start = state.workspace?.path || state.workspaces?.cwd || "";
3525
+ void pickFolderInSystemWindow(start, el.workspaceFeedback).then((picked) => {
3526
+ if (picked === null) {
3527
+ el.workspaceDialog.close();
3528
+ openFolderDialog({ start, onPick: (path) => openWorkspace(path) });
3529
+ return;
3530
+ }
3531
+ if (!picked) return;
3532
+ el.workspaceDialog.close();
3533
+ openWorkspace(picked);
3534
+ });
3368
3535
  });
3369
3536
  el.agentsOpen.addEventListener("click", () => openAgentsDialog());
3370
3537
  el.agentMigrate.addEventListener("click", () => migrateSelectedAgentSession());
@@ -3386,8 +3553,9 @@ el.newChatForm.addEventListener("submit", (event) => {
3386
3553
  submitNewChat();
3387
3554
  });
3388
3555
  el.newChatBrowse.addEventListener("click", () => {
3389
- openFolderDialog({
3556
+ void chooseFolder({
3390
3557
  start: el.newChatCwd.value.trim() || state.workspace?.path || "",
3558
+ feedback: el.newChatFeedback,
3391
3559
  onPick: (path) => {
3392
3560
  el.newChatCwd.value = path;
3393
3561
  renderNewChatRecents();
@@ -3475,3 +3643,24 @@ loadSeen().then(() => loadOverview()).then(async () => {
3475
3643
  if (state.workspace && !state.selection) renderWorkspaceLanding(state.workspace.path);
3476
3644
  });
3477
3645
  setInterval(() => loadOverview({ silent: true }), REFRESH_MS);
3646
+
3647
+ // The plugin shell consumes the same public conversation projection as the built-in UI.
3648
+ if (typeof createPluginUI === "function") pluginUI = createPluginUI({
3649
+ profile: PRO_MODE ? "pro" : "standard",
3650
+ api: apiFetch,
3651
+ context: () => {
3652
+ const data = state.snapshot ?? state.fileData ?? state.agentData ?? {};
3653
+ const selection = state.selection;
3654
+ return { identity:selection ? `${selection.kind}:${selection.key ?? selection.id ?? selection.cwd ?? ""}` : "",
3655
+ cwd:data.cwd ?? selection?.cwd ?? state.workspace?.path ?? "",messages:data.messages ?? [],
3656
+ plan:data.plan,model:data.model,streamingContent:data.streamingContent,pluginUI:data.task?.pluginUI,pluginPrompt:data.task?.pluginPrompt,pluginErrors:data.task?.pluginErrors };
3657
+ },
3658
+ getDraft: () => el.input.value,
3659
+ setDraft: text => { el.input.value=text; autosize(); },
3660
+ submit: async () => { if (!state.selection) throw new Error("Open a conversation first"); await sendMessage({ selection:state.selection,text:el.input.value }); },
3661
+ command: async (name,args) => {
3662
+ if (!state.selection) throw new Error("Open a conversation first");
3663
+ await sendMessage({ selection:state.selection,text:`/plugin ${name} ${JSON.stringify(args)}`,preserveDraft:true });
3664
+ },
3665
+ notice: message => { el.sub.textContent=message; },
3666
+ });
package/web/i18n.js CHANGED
@@ -81,11 +81,24 @@ export function locale() {
81
81
  const ZH = {
82
82
  "Agent mode": "Agent 模式",
83
83
  "Standard mode": "标准模式",
84
- "PTC mode": "PTC模式",
84
+ "PTC mode": "PTC 模式",
85
85
  "Fast mode": "快速模式",
86
+ "Fast Mode": "极速模式",
87
+ "Fast Mode is disabled; select another mode.": "极速模式已关闭,请选择其他模式。",
88
+ "Dynamic task scheduling and concurrent execution shorten complex tasks while preserving reasoning capability.": "通过动态任务调度与多任务并发执行,大幅缩短复杂任务的完成时间,同时保持原有推理能力。",
89
+ "Planning tasks": "正在规划任务",
90
+ "Running tasks concurrently": "正在并发执行",
91
+ "Verifying local results": "正在执行局部验证",
92
+ "Integrating results": "正在整合结果",
93
+ "Final global review": "正在进行最终审查",
94
+ "Fast Mode completed": "极速模式已完成",
95
+ "Fast Mode failed": "极速模式执行失败",
96
+ "Fast Mode cancelled": "极速模式已取消",
97
+ "{running} running · {verified}/{total} verified": "{running} 个正在运行 · {verified}/{total} 个已验证",
98
+ "Select Standard mode to continue": "选择标准模式继续",
86
99
  "Professional mode": "专业模式",
87
- "Full-featured Agent for every scenario": "功能完整的Agent,适用于所有场景",
88
- "All standard capabilities, with code-based planning to save tokens": "拥有标准模式所有功能,通过代码实现规划,节省Token",
100
+ "Full-featured Agent for every scenario": "功能完整的 Agent ,适用于所有场景",
101
+ "All standard capabilities, with code-based planning to save tokens": "拥有标准模式所有功能,通过代码实现规划,节省 Token ",
89
102
  "Less thinking and fewer calls for quick answers": "减少思考和调用,迅速回答问题",
90
103
  "Assign tasks to the best-suited models so multiple models complement each other": "适合需要多个模型取长补短的任务,将任务分给最适合他的模型",
91
104
  "Applies to the next turn": "从下一轮开始生效",
@@ -180,6 +193,8 @@ const ZH = {
180
193
  "Open folder…": "打开文件夹…",
181
194
  "Open working directory": "打开工作目录",
182
195
  "Choose a folder to open as the working directory.": "选择一个文件夹作为工作目录。",
196
+ "Select a working directory": "选择工作目录",
197
+ "Waiting for the folder window on your desktop…": "正在等待桌面上的文件夹窗口…",
183
198
  "This folder no longer exists.": "此文件夹已不存在。",
184
199
  "{n} conversations": "{n} 个对话",
185
200
  "Recent folders": "最近文件夹",
package/web/index.html CHANGED
@@ -25,6 +25,7 @@
25
25
  <title>Neutrino</title>
26
26
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f9fafb' stroke-width='1.7'%3E%3Cellipse cx='12' cy='12' rx='9.4' ry='3.8' transform='rotate(-30 12 12)'/%3E%3Ccircle cx='12' cy='12' r='3.1'/%3E%3C/svg%3E" />
27
27
  <link rel="stylesheet" href="style.css" />
28
+ <link rel="stylesheet" href="plugins.css" />
28
29
  </head>
29
30
  <body>
30
31
  <div id="app">
@@ -135,6 +136,7 @@
135
136
  <button type="button" role="menuitemradio" aria-checked="true" data-agent-mode="standard"><span><strong data-i18n="Standard mode">Standard mode</strong><small data-i18n="Full-featured Agent for every scenario">Full-featured Agent for every scenario</small></span><svg class="agent-mode-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg></button>
136
137
  <button type="button" role="menuitemradio" aria-checked="false" data-agent-mode="ptc"><span><strong data-i18n="PTC mode">PTC mode</strong><small data-i18n="All standard capabilities, with code-based planning to save tokens">All standard capabilities, with code-based planning to save tokens</small></span><svg class="agent-mode-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg></button>
137
138
  <button type="button" role="menuitemradio" aria-checked="false" data-agent-mode="fast"><span><strong data-i18n="Fast mode">Fast mode</strong><small data-i18n="Less thinking and fewer calls for quick answers">Less thinking and fewer calls for quick answers</small></span><svg class="agent-mode-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg></button>
139
+ <button class="fast-mode-card" type="button" role="menuitemradio" aria-checked="false" data-agent-mode="fast_dag"><span><strong data-i18n="Fast Mode">Fast Mode</strong><small data-i18n="Dynamic task scheduling and concurrent execution shorten complex tasks while preserving reasoning capability.">Dynamic task scheduling and concurrent execution shorten complex tasks while preserving reasoning capability.</small></span><svg class="agent-mode-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg></button>
138
140
  <button type="button" role="menuitemradio" aria-checked="false" data-agent-mode="pro"><span><strong data-i18n="Professional mode">Professional mode</strong><small data-i18n="Assign tasks to the best-suited models so multiple models complement each other">Assign tasks to the best-suited models so multiple models complement each other</small></span><svg class="agent-mode-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg></button>
139
141
  </div>
140
142
  <p id="agent-mode-feedback" class="agent-mode-feedback" role="status" aria-live="polite"></p>
@@ -182,7 +184,7 @@
182
184
  </div>
183
185
  </details>
184
186
  <button id="send" class="primary" type="button" title="Send message" aria-label="Send message" data-i18n-title="Send message" data-i18n-aria-label="Send message">
185
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path class="send-icon" d="M12 19V5M5 12l7-7 7 7" /><rect class="pause-icon" x="5" y="5" width="14" height="14" rx="2" fill="currentColor" stroke="none" /></svg>
187
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path class="send-icon" d="M12 19V5M5 12l7-7 7 7" /><rect class="pause-icon" x="5" y="5" width="14" height="14" rx="2" fill="currentColor" stroke="none" /><path class="resume-icon" d="M8 5.6v12.8L19 12z" fill="currentColor" stroke="none" /></svg>
186
188
  </button>
187
189
  </div>
188
190
  <div class="composer-hint">
@@ -0,0 +1,10 @@
1
+ .plugin-manager-body{display:flex;flex-direction:column;gap:14px}.plugin-dialog{width:min(860px,94vw)}
2
+ .plugin-list{display:grid;gap:12px}.plugin-card{padding:16px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--surface-1)}
3
+ .plugin-card-heading,.plugin-actions,.plugin-install{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.plugin-card-heading{justify-content:space-between}.plugin-card code{font-size:12px;color:var(--text-caption)}
4
+ .plugin-card p{margin:9px 0}.plugin-badge{font-size:11px;color:var(--accent);border:1px solid var(--accent-line);border-radius:20px;padding:3px 8px}.plugin-warning{color:var(--amber);font-size:13px}
5
+ .plugin-config,.plugin-providers{display:grid;gap:10px;margin-top:12px}.plugin-field{display:flex;justify-content:space-between;align-items:center;gap:12px;font-size:13px}.plugin-field>span{flex:1}.plugin-field select,.plugin-field input:not([type=checkbox]){max-width:55%;min-width:140px}.plugin-field textarea{max-width:60%;min-height:90px;color:var(--text);background:var(--surface-code);border:1px solid var(--border)}
6
+ .plugin-install input{flex:1}.plugin-details{white-space:pre-wrap;overflow-wrap:anywhere;font-size:11px;max-height:280px;overflow:auto}.plugin-card summary{cursor:pointer;font-size:13px;margin-top:12px}
7
+ .plugin-frame{width:100%;display:block;border:0;min-height:40px;background:transparent}.plugin-tabs,.plugin-toolbar,.plugin-header-actions,.plugin-menu{display:flex;flex-wrap:wrap;gap:7px}.plugin-tabs:empty,.plugin-toolbar:empty,.plugin-menu:empty{display:none}.plugin-tabs{padding:8px 22px}.plugin-tabs .control{font-size:12px}.plugin-toolbar{padding:0 0 7px}.plugin-sidebar{padding:8px 16px}.plugin-sidebar:empty{display:none}
8
+ .plugin-panel{margin:8px 22px;padding:12px;background:var(--surface-1);border:1px solid var(--border);border-radius:var(--radius-md)}.plugin-panel-content{margin-top:10px}.plugin-skill-select{max-width:260px;font-size:12px}.plugin-composer-target{width:100%}
9
+ .composer-card{border-radius:var(--neutrino-composer-radius,var(--radius-xl))}.message{border-radius:var(--neutrino-message-radius,0)}
10
+ @media(max-width:640px){.plugin-field{align-items:flex-start;flex-direction:column}.plugin-field input:not([type=checkbox]),.plugin-field select,.plugin-field textarea{max-width:100%;width:100%}.plugin-panel{margin:6px}.plugin-actions .control{font-size:12px}}