@xingjisoft/neutrino 0.5.2 → 0.6.2

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.
package/web/i18n.js CHANGED
@@ -79,6 +79,21 @@ export function locale() {
79
79
  }
80
80
 
81
81
  const ZH = {
82
+ "Agent mode": "Agent 模式",
83
+ "Standard mode": "标准模式",
84
+ "PTC mode": "PTC 模式",
85
+ "Fast mode": "快速模式",
86
+ "Professional mode": "专业模式",
87
+ "Full-featured Agent for every scenario": "功能完整的 Agent ,适用于所有场景",
88
+ "All standard capabilities, with code-based planning to save tokens": "拥有标准模式所有功能,通过代码实现规划,节省 Token ",
89
+ "Less thinking and fewer calls for quick answers": "减少思考和调用,迅速回答问题",
90
+ "Assign tasks to the best-suited models so multiple models complement each other": "适合需要多个模型取长补短的任务,将任务分给最适合他的模型",
91
+ "Applies to the next turn": "从下一轮开始生效",
92
+ "Changing mode…": "正在切换模式…",
93
+ "Could not change mode: {error}": "无法切换模式:{error}",
94
+ "Select a working directory first.": "请先选择工作目录。",
95
+ "Program": "程序",
96
+
82
97
  "Pro preset": "Pro 预设",
83
98
  "Preset": "预设",
84
99
  "Neutrino Pro": "Neutrino Pro",
@@ -218,6 +233,7 @@ const ZH = {
218
233
  "Choose a conversation from the sidebar to view it.": "从侧边栏选择一个对话查看。",
219
234
  "No conversations yet.": "暂无对话。",
220
235
  "No conversations in this folder yet.": "此文件夹中暂无对话。",
236
+ "New conversation in this folder": "在此文件夹中新建对话",
221
237
  "Type a message below to start a conversation in this folder.": "在下方输入消息,即可在此文件夹中开始对话。",
222
238
  "Send a message to get started.": "发送一条消息开始对话。",
223
239
  "This conversation is empty.": "此对话为空。",
@@ -268,6 +284,10 @@ const ZH = {
268
284
 
269
285
  // -- live status --------------------------------------------------------
270
286
  "paused": "已暂停",
287
+ "Stopping tasks…": "正在停止任务…",
288
+ "Blocked": "受阻",
289
+ "Cancelled": "已取消",
290
+ "Skipped": "已跳过",
271
291
  "working": "处理中",
272
292
  "idle": "空闲",
273
293
  "compressing": "正在压缩上下文",
package/web/index.html CHANGED
@@ -129,6 +129,16 @@
129
129
  <div class="composer-inner">
130
130
  <div class="composer-card">
131
131
  <textarea id="input" rows="1" placeholder="Message Neutrino…" aria-label="Message" data-i18n-placeholder="Message Neutrino…"></textarea>
132
+ <details id="agent-mode-picker" class="agent-mode-picker">
133
+ <summary aria-label="Agent mode" data-i18n-aria-label="Agent mode"><strong id="agent-mode-label" data-i18n="Standard mode">Standard mode</strong><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m7 10 5 5 5-5" /></svg></summary>
134
+ <div class="agent-mode-menu" role="menu" aria-label="Agent mode" data-i18n-aria-label="Agent mode">
135
+ <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
+ <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
+ <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>
138
+ <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
+ </div>
140
+ <p id="agent-mode-feedback" class="agent-mode-feedback" role="status" aria-live="polite"></p>
141
+ </details>
132
142
  <details id="model-picker" class="model-picker pro-preset-control hidden">
133
143
  <summary id="model-picker-summary" aria-label="Conversation model" data-i18n-aria-label="Conversation model"><span id="model-picker-label"></span><svg class="pro-preset-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7 10 5 5 5-5" /></svg></summary>
134
144
  <div class="pro-preset-card model-picker-card">
package/web/style.css CHANGED
@@ -413,11 +413,20 @@ svg {
413
413
  margin-bottom: 4px;
414
414
  }
415
415
 
416
+ /* A folder row is two controls: the name folds the list, the plus beside it
417
+ opens a new conversation in that folder. */
418
+ .group-head {
419
+ display: flex;
420
+ align-items: center;
421
+ gap: 2px;
422
+ }
423
+
416
424
  .group-header {
417
425
  display: flex;
418
426
  align-items: center;
419
427
  gap: 7px;
420
- width: 100%;
428
+ flex: 1;
429
+ min-width: 0;
421
430
  height: 30px;
422
431
  padding: 0 8px;
423
432
  background: transparent;
@@ -461,16 +470,22 @@ svg {
461
470
  font-size: 12px;
462
471
  }
463
472
 
464
- .group-header .count {
465
- flex: none;
466
- min-width: 20px;
467
- padding: 0 6px;
468
- border-radius: 999px;
469
- background: var(--surface-1);
473
+ /* Small enough to sit on the folder row without competing with the name, and
474
+ dimmed so a long list of folders never reads as a wall of plus signs. */
475
+ .group-new {
476
+ width: 22px;
477
+ height: 22px;
470
478
  color: var(--text-caption);
471
- font-size: 10.5px;
472
- line-height: 18px;
473
- text-align: center;
479
+ }
480
+
481
+ .group-new svg {
482
+ width: 13px;
483
+ height: 13px;
484
+ }
485
+
486
+ .group-new:hover {
487
+ background: var(--surface-1);
488
+ color: var(--text-secondary);
474
489
  }
475
490
 
476
491
  .group-items {
@@ -1355,7 +1370,7 @@ svg {
1355
1370
  transform: rotate(90deg);
1356
1371
  }
1357
1372
 
1358
- .turn-you {
1373
+ .turn-label {
1359
1374
  flex: 1 1 auto;
1360
1375
  min-width: 0;
1361
1376
  overflow: hidden;
@@ -1474,6 +1489,26 @@ svg {
1474
1489
  outline: none;
1475
1490
  }
1476
1491
 
1492
+ .agent-mode-picker { position: relative; align-self: center; margin-right: auto; min-width: 0; }
1493
+ .agent-mode-picker > summary { display: flex; align-items: center; gap: 7px; padding: 7px 9px; border-radius: var(--radius-sm); cursor: pointer; list-style: none; color: var(--text-secondary); font-size: 12px; }
1494
+ .agent-mode-picker > summary::-webkit-details-marker { display: none; }
1495
+ .agent-mode-picker > summary strong { font-weight: 600; }
1496
+ .agent-mode-picker > summary svg { width: 14px; height: 14px; }
1497
+ .agent-mode-picker > summary:hover, .agent-mode-picker[open] > summary { background: var(--surface-3); }
1498
+ .agent-mode-picker.is-pending > summary { color: var(--accent); }
1499
+ .agent-mode-menu { position: absolute; z-index: 35; left: 0; bottom: calc(100% + 10px); width: min(370px, calc(100vw - 48px)); max-height: min(520px, 60dvh); overflow-y: auto; padding: 6px; border: 1px solid var(--border-strong); border-radius: var(--radius-lg); background: var(--surface-1); box-shadow: var(--shadow-strong); }
1500
+ .agent-mode-menu > button { display: flex; align-items: center; gap: 12px; width: 100%; padding: 11px 12px; text-align: left; color: var(--text); background: transparent; border: 0; border-radius: var(--radius-sm); cursor: pointer; font-family: var(--font); }
1501
+ .agent-mode-menu > button:hover, .agent-mode-menu > button:focus-visible, .agent-mode-menu > button[aria-checked="true"] { background: var(--surface-3); }
1502
+ .agent-mode-menu > button > span { flex: 1; min-width: 0; }
1503
+ .agent-mode-menu strong { display: block; margin-bottom: 4px; font-size: 13px; font-weight: 700; }
1504
+ .agent-mode-menu small { display: block; color: var(--text-tertiary); font-size: 12px; font-weight: 400; line-height: 1.55; }
1505
+ .agent-mode-check { flex: none; width: 16px; height: 16px; color: var(--accent); visibility: hidden; }
1506
+ .agent-mode-menu [aria-checked="true"] .agent-mode-check { visibility: visible; }
1507
+ .agent-mode-menu > button:disabled { opacity: .55; cursor: wait; }
1508
+ .agent-mode-feedback { position: absolute; bottom: 100%; left: 0; width: min(370px, calc(100vw - 48px)); margin: 0 0 5px; font-size: 11px; color: var(--text-secondary); }
1509
+ .agent-mode-feedback:empty, .agent-mode-picker[open] .agent-mode-feedback { display: none; }
1510
+ .agent-mode-feedback.error { color: var(--error, #ef8181); }
1511
+ .program-source { border-bottom: 1px solid var(--border-soft); }
1477
1512
  .pro-preset-control { position: relative; margin-left: auto; align-self: center; }
1478
1513
  .pro-preset-control > summary {
1479
1514
  display: flex; align-items: center; gap: 10px; padding: 7px 10px;
@@ -3308,6 +3343,13 @@ html.pro-transitioning body::before {
3308
3343
  min-height: 40px;
3309
3344
  }
3310
3345
 
3346
+ /* The plus takes the same 40px target as the other icon buttons here, so
3347
+ its icon grows with it instead of staying at the desktop size. */
3348
+ .group-new svg {
3349
+ width: 16px;
3350
+ height: 16px;
3351
+ }
3352
+
3311
3353
  .main-header {
3312
3354
  display: grid;
3313
3355
  grid-template-columns: 40px minmax(0, 1fr);
@@ -3533,3 +3575,7 @@ html.pro-transitioning body::before {
3533
3575
  min-height: 64px;
3534
3576
  }
3535
3577
  }
3578
+
3579
+ .plan-step.blocked .plan-node,
3580
+ .plan-step.blocked .plan-step-title strong { color: var(--orange, #dca548); }
3581
+ .plan-step.cancelled, .plan-step.skipped { opacity: .65; }
package/web/turns.js CHANGED
@@ -2,9 +2,9 @@
2
2
  //
3
3
  // A conversation is a sequence of turns: each one starts at a user message and
4
4
  // runs until the next one. Once the agent has finished a turn, the browser folds
5
- // away the work it took — the prompt body, the thinking and the tool calls — and
6
- // keeps the final reply on screen, so a finished exchange always ends on its
7
- // answer. The complete round is still one click away.
5
+ // away the work it took — the thinking and the tool calls — and keeps the full
6
+ // user prompt and final reply on screen. The complete round is still one click
7
+ // away.
8
8
  //
9
9
  // The thresholds mirror TURN_FOLD_MIN_BLOCKS / TURN_FOLD_MIN_CHARS in
10
10
  // src/screens/chat.ts, so the terminal and the browser fold the same turns.
@@ -33,8 +33,8 @@ export function turnAnswerIndex(messages) {
33
33
 
34
34
  /**
35
35
  * True when folding the work behind this turn saves more room than it costs.
36
- * The reply is never folded away, so it is not counted: a long answer on its own
37
- * must never collapse the turn that carries it.
36
+ * User input and the reply are never folded away, so neither counts toward the
37
+ * folding threshold, however long either one is.
38
38
  */
39
39
  export function turnFolds(messages) {
40
40
  const reply = turnAnswerIndex(messages);
@@ -42,7 +42,7 @@ export function turnFolds(messages) {
42
42
  let chars = 0;
43
43
  messages.forEach((message, index) => {
44
44
  if (index === reply) return;
45
- if (message.role !== "user" && message.role !== "assistant") return;
45
+ if (message.role !== "assistant") return;
46
46
  blocks++;
47
47
  chars += String(message.content ?? "").length;
48
48
  });
@@ -50,8 +50,7 @@ export function turnFolds(messages) {
50
50
  }
51
51
 
52
52
  /**
53
- * The first line of the first message in `role`. One line only: the header is a
54
- * signpost, and the full text is one click away.
53
+ * The first nonempty line of the first message in `role` that contains text.
55
54
  */
56
55
  export function turnPreview(messages, role) {
57
56
  for (const message of messages) {