@aragon-agent/cli 0.6.11 → 0.6.12
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/CHANGELOG.md +10 -1
- package/README.md +24 -13
- package/dist/agent/controller.d.ts +9 -2
- package/dist/agent/controller.js +29 -39
- package/dist/agent/queued-messages.d.ts +8 -0
- package/dist/agent/queued-messages.js +28 -0
- package/dist/agent/reducer.d.ts +8 -1
- package/dist/agent/reducer.js +23 -34
- package/dist/commands/builtins.js +21 -7
- package/dist/input/enter-sequences.d.ts +11 -10
- package/dist/input/enter-sequences.js +29 -27
- package/dist/input/stdin-filter.js +90 -26
- package/dist/logging/install.d.ts +3 -0
- package/dist/session/persist.d.ts +6 -7
- package/dist/session/persist.js +6 -9
- package/dist/session/store.d.ts +4 -1
- package/dist/session/validate-session.d.ts +3 -0
- package/dist/session/validate-session.js +207 -0
- package/dist/ui/App.js +55 -28
- package/dist/ui/Composer.d.ts +2 -1
- package/dist/ui/PromptInput.d.ts +2 -1
- package/dist/ui/PromptInput.js +50 -71
- package/dist/ui/QueueStatusRow.d.ts +22 -0
- package/dist/ui/QueueStatusRow.js +51 -0
- package/dist/ui/StatusBar.d.ts +2 -0
- package/dist/ui/StatusBar.js +30 -0
- package/dist/ui/composer-input.d.ts +28 -0
- package/dist/ui/composer-input.js +46 -0
- package/dist/ui/editor-reducer.d.ts +5 -0
- package/dist/ui/editor-reducer.js +2 -0
- package/dist/ui/enter-frames.d.ts +15 -31
- package/dist/ui/enter-frames.js +14 -33
- package/dist/ui/overlays/HelpOverlay.js +8 -0
- package/dist/ui/selection/selection-controller.js +57 -20
- package/dist/ui/transcript-text.js +20 -12
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,16 @@ All notable changes to `@aragon-agent/cli` are documented here. Entries at
|
|
|
5
5
|
— those releases really were published under that name, so they are left as they
|
|
6
6
|
were written.
|
|
7
7
|
|
|
8
|
-
## Unreleased
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
### TUI 输入与排队可靠性
|
|
11
|
+
|
|
12
|
+
- Shift+Enter 与粘贴按输入顺序处理;普通 Enter 保持弹层兼容,混合输入最多提交一次,
|
|
13
|
+
余稿及拒绝提交的正文保留。结束输入流时完整排空,不泄漏内部控制标记。
|
|
14
|
+
- 超限粘贴拒绝同一外部输入块的正文和 Enter,避免误提交旧稿;未闭合粘贴继续按正文消费。
|
|
15
|
+
- 拖选后仅 Ctrl+C 复制;文字变化与丢失鼠标释放时取消无效高亮,避免卡住视口。
|
|
16
|
+
- 固定 Queue 状态行按消息 ID 回执更新,支持窄屏、暂停与多条消息计数。
|
|
17
|
+
- 保存和退出保留所有未发送全文;恢复先验证存档,旧 queued 作为警告展示且不自动重放。
|
|
9
18
|
|
|
10
19
|
### Removed
|
|
11
20
|
|
package/README.md
CHANGED
|
@@ -493,10 +493,10 @@ is what it used to do. Inline mode is unaffected either way.
|
|
|
493
493
|
|
|
494
494
|
## Terminal setup for Shift+Enter
|
|
495
495
|
|
|
496
|
-
`Ctrl+J`
|
|
497
|
-
|
|
498
|
-
`\u001b[13;2u
|
|
499
|
-
|
|
496
|
+
输入支持 `Ctrl+J`、`Alt+Enter` 和可独立编码的 `Shift+Enter` 换行。如果终端把
|
|
497
|
+
Shift+Enter 与 Enter 都发送为 CR,应用无法区分;请用 `/terminal-setup` 查看绑定,
|
|
498
|
+
让 Shift+Enter 发送 `\u001b[13;2u`。合并到已有终端配置,不要覆盖其他快捷键;
|
|
499
|
+
终端拦截某个组合键时可使用另外两种方式。参考绑定如下:
|
|
500
500
|
|
|
501
501
|
- **Windows Terminal** — `settings.json`, `actions`:
|
|
502
502
|
`{ "keys": "shift+enter", "command": { "action": "sendInput", "input": "\u001b[13;2u" } }`
|
|
@@ -518,20 +518,31 @@ user-side binding reaches the same result for the one key that needs it.
|
|
|
518
518
|
|
|
519
519
|
## Queued messages while the agent runs
|
|
520
520
|
|
|
521
|
-
|
|
522
|
-
|
|
521
|
+
Agent 运行中发送的消息会进入队列。固定状态行显示最早待接收消息的首个非空行,
|
|
522
|
+
多条时显示剩余数量;即使转录滚动离开底部,提示仍可见,且没有自动消失的时间限制。
|
|
523
|
+
转录同时保留每条完整消息:
|
|
523
524
|
|
|
524
525
|
```
|
|
525
526
|
◷ Queue: also check the tests
|
|
526
527
|
```
|
|
527
528
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
529
|
+
仅当 Core 将消息写入对话历史并发出对应消息 ID 的接收回执,Queue 状态才移除。
|
|
530
|
+
这表示引擎已接管后续处理,不表示回答完成;`turn_start`、动画和提示超时都不能代替回执。
|
|
531
|
+
相同正文的两次发送仍是两条消息。中断或失败后尚未接收的消息显示 `Queue (paused)`。
|
|
532
|
+
|
|
533
|
+
窄屏使用 `P>B` / `B>P` 表示待切换模式,`E2` / `E!` 表示两次 Esc 中断 / 再按 Esc 强停,
|
|
534
|
+
`S+` / `T+` 表示后台服务 / TODO 存在,`Queue(p)` 表示暂停;`(+99+)` 表示至少还有
|
|
535
|
+
100 条消息。完整服务和 TODO 数量分别可用 `/bg` 与 `/todo status` 查看。
|
|
536
|
+
|
|
537
|
+
`/clear` 清转录和 TODO,但保留待接收队列。保存和退出会补回被清屏或保留环裁剪的
|
|
538
|
+
未发送全文。恢复存档时,queued 一律成为 `Queued but never sent: ...` 警告,
|
|
539
|
+
不会自动重发;需要发送时请复制正文重新提交。运行尚未退出时 `/reset`、`/resume`
|
|
540
|
+
会拒绝切换,损坏存档在替换当前会话之前报错。
|
|
541
|
+
|
|
542
|
+
应用捕获鼠标时,拖选只保留高亮,按 Ctrl+C 才复制;本次按键不会停止服务或退出。
|
|
543
|
+
选中文字改变、滚轮、其他按键或关闭选区会取消高亮。`--no-mouse` 或宿主原生选择的
|
|
544
|
+
选中即复制由终端设置控制。OSC 52 发送成功仅证明请求已发出,不保证远程终端剪贴板
|
|
545
|
+
已写入;SSH/tmux 是否允许该请求需在实际环境中验证。
|
|
535
546
|
|
|
536
547
|
## Themes
|
|
537
548
|
|
|
@@ -214,8 +214,11 @@ export declare class AgentController {
|
|
|
214
214
|
* Cleared at the next `agent_start`.
|
|
215
215
|
*/
|
|
216
216
|
private abortRequested;
|
|
217
|
-
/**
|
|
218
|
-
private
|
|
217
|
+
/** Only exact acceptance receipts release user messages from reviewer protection. */
|
|
218
|
+
private readonly pendingUserSteering;
|
|
219
|
+
/** Session changes must not make a delayed receipt match a new message. */
|
|
220
|
+
private readonly steeringPrefix;
|
|
221
|
+
private steeringSequence;
|
|
219
222
|
constructor(config: CliConfig, deps?: ControllerDeps);
|
|
220
223
|
private resolveKey;
|
|
221
224
|
/**
|
|
@@ -360,7 +363,11 @@ export declare class AgentController {
|
|
|
360
363
|
* which it has to be, since a normal quit reaches it twice.
|
|
361
364
|
*/
|
|
362
365
|
reapServicesSync(): void;
|
|
366
|
+
/** Queue automation/user guidance with the same protection as visible input. */
|
|
363
367
|
steer(text: string): void;
|
|
368
|
+
/** Return the opaque ID used by the view to match Core's acceptance receipt. */
|
|
369
|
+
queueUserMessage(text: string): string;
|
|
370
|
+
private enqueueSteering;
|
|
364
371
|
followUp(text: string): void;
|
|
365
372
|
isRunning(): boolean;
|
|
366
373
|
getAgentMode(): AgentMode;
|
package/dist/agent/controller.js
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* AgentController — builds the core `Agent`, wires the built-in toolset, and
|
|
3
|
-
* exposes a small run/abort/steer surface plus the pre-flight validation that
|
|
4
|
-
* turns the most common failure (no API key) into a guided action instead of a
|
|
5
|
-
* blank screen (spec §3.3.1 / R2).
|
|
6
|
-
*
|
|
7
|
-
* The controller never inspects the resolved promise of `agent.prompt()` for
|
|
8
|
-
* success — that promise resolves even on failure. Success/failure is derived
|
|
9
|
-
* from the event stream by the reducer (TUI) and the headless writer.
|
|
10
|
-
*/
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
11
2
|
import { Agent, initProviders, ModelRegistry, } from '@aragon-agent/core';
|
|
12
3
|
import { clampCompactionConfig, clampFastConfig, clampRetryConfig, clampTeamConfig, clampTodoConfig, isAdapterProvider, toRetryPolicy, } from '../config/schema.js';
|
|
13
4
|
import { makeGetApiKey } from '../config/load.js';
|
|
@@ -252,8 +243,11 @@ export class AgentController {
|
|
|
252
243
|
* Cleared at the next `agent_start`.
|
|
253
244
|
*/
|
|
254
245
|
abortRequested = false;
|
|
255
|
-
/**
|
|
256
|
-
|
|
246
|
+
/** Only exact acceptance receipts release user messages from reviewer protection. */
|
|
247
|
+
pendingUserSteering = new Set();
|
|
248
|
+
/** Session changes must not make a delayed receipt match a new message. */
|
|
249
|
+
steeringPrefix = randomUUID();
|
|
250
|
+
steeringSequence = 0;
|
|
257
251
|
constructor(config, deps = {}) {
|
|
258
252
|
this.config = config;
|
|
259
253
|
this.cwd = config.cwd;
|
|
@@ -634,33 +628,26 @@ export class AgentController {
|
|
|
634
628
|
isPricedModel: (ref) => this.isPricedModel(ref),
|
|
635
629
|
subscribe: (listener) => this.agent.subscribe(listener),
|
|
636
630
|
complete: (providerId, request) => this.providerRegistry.complete(providerId, request),
|
|
637
|
-
//
|
|
638
|
-
|
|
639
|
-
// `userSteerCount` would otherwise mis-report and what would make
|
|
640
|
-
// guard 2 clear a queue it does not own (D-21).
|
|
641
|
-
//
|
|
642
|
-
// It is decremented back out below: the counter must answer "did the
|
|
643
|
-
// USER queue anything", and the reviewer's own message is not that.
|
|
644
|
-
steer: (text) => {
|
|
645
|
-
this.steer(text);
|
|
646
|
-
this.userSteerCount -= 1;
|
|
647
|
-
},
|
|
631
|
+
// Reviewer advice shares skill activation, but cannot own a user ID.
|
|
632
|
+
steer: (text) => this.enqueueSteering(text, false),
|
|
648
633
|
isRunning: () => this.isRunning(),
|
|
649
634
|
isAbortRequested: () => this.abortRequested,
|
|
650
|
-
userSteerCount: () => this.
|
|
635
|
+
userSteerCount: () => this.pendingUserSteering.size,
|
|
651
636
|
clearAllQueues: () => this.clearAllQueues(),
|
|
652
637
|
notify: (level, text) => deps.notify?.(level, text),
|
|
653
638
|
onPromptChanged: () => this.rebuildSystemPrompt(),
|
|
654
639
|
})
|
|
655
640
|
: null;
|
|
656
641
|
// The two guards' bookkeeping, and the only reason this class subscribes to
|
|
657
|
-
// its own agent.
|
|
642
|
+
// its own agent. A receipt is where a user steer stops being pending;
|
|
658
643
|
// `agent_start` is where a requested abort stops being in force.
|
|
659
644
|
this.agent.subscribe((event) => {
|
|
660
645
|
if (event.type === 'agent_start')
|
|
661
646
|
this.abortRequested = false;
|
|
662
|
-
if (event.type === '
|
|
663
|
-
|
|
647
|
+
if (event.type === 'steering_accepted') {
|
|
648
|
+
for (const id of event.ids)
|
|
649
|
+
this.pendingUserSteering.delete(id);
|
|
650
|
+
}
|
|
664
651
|
// The authoritative result arrives with this event, so from this instant
|
|
665
652
|
// the tail is superseded and its slot must be released (§3.1.1). Clearing
|
|
666
653
|
// HERE rather than on read is what keeps the store's size a function of
|
|
@@ -1052,21 +1039,23 @@ export class AgentController {
|
|
|
1052
1039
|
reapServicesSync() {
|
|
1053
1040
|
this.procs.reapSync();
|
|
1054
1041
|
}
|
|
1042
|
+
/** Queue automation/user guidance with the same protection as visible input. */
|
|
1055
1043
|
steer(text) {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1044
|
+
this.enqueueSteering(text, true);
|
|
1045
|
+
}
|
|
1046
|
+
/** Return the opaque ID used by the view to match Core's acceptance receipt. */
|
|
1047
|
+
queueUserMessage(text) {
|
|
1048
|
+
return this.enqueueSteering(text, true);
|
|
1049
|
+
}
|
|
1050
|
+
enqueueSteering(text, user) {
|
|
1051
|
+
// Both reviewer and user guidance can activate queued skill frames.
|
|
1060
1052
|
if (this.skillsEnabled)
|
|
1061
1053
|
this.skills.absorbPendingFrames();
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
// leave its own residue rather than destroy something the user typed.
|
|
1068
|
-
this.userSteerCount += 1;
|
|
1069
|
-
this.agent.steer(text);
|
|
1054
|
+
const id = user ? `${this.steeringPrefix}:${++this.steeringSequence}` : undefined;
|
|
1055
|
+
if (id !== undefined)
|
|
1056
|
+
this.pendingUserSteering.add(id);
|
|
1057
|
+
this.agent.steer(text, id);
|
|
1058
|
+
return id;
|
|
1070
1059
|
}
|
|
1071
1060
|
followUp(text) {
|
|
1072
1061
|
this.agent.followUp(text);
|
|
@@ -1524,6 +1513,7 @@ export class AgentController {
|
|
|
1524
1513
|
}
|
|
1525
1514
|
clearAllQueues() {
|
|
1526
1515
|
this.agent.clearAllQueues();
|
|
1516
|
+
this.pendingUserSteering.clear();
|
|
1527
1517
|
}
|
|
1528
1518
|
// -----------------------------------------------------------------------
|
|
1529
1519
|
// Model / provider info
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Entry } from './reducer.js';
|
|
2
|
+
/** Full user text retained until Core acknowledges its opaque ID. */
|
|
3
|
+
export interface PendingSteering {
|
|
4
|
+
readonly queueId: string;
|
|
5
|
+
readonly text: string;
|
|
6
|
+
}
|
|
7
|
+
/** Supplement trimmed/cleared history for save and exit without deduplicating text. */
|
|
8
|
+
export declare function mergePendingEntries(entries: Entry[], pending: readonly PendingSteering[]): Entry[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Supplement trimmed/cleared history for save and exit without deduplicating text. */
|
|
2
|
+
export function mergePendingEntries(entries, pending) {
|
|
3
|
+
if (pending.length === 0)
|
|
4
|
+
return entries;
|
|
5
|
+
const positions = new Map(pending.map((item, index) => [item.queueId, index]));
|
|
6
|
+
const visible = new Map(entries.flatMap((entry) => entry.kind === 'queued' && entry.queueId !== undefined
|
|
7
|
+
? [[entry.queueId, entry]] : []));
|
|
8
|
+
const merged = [];
|
|
9
|
+
let cursor = 0;
|
|
10
|
+
const appendThrough = (last) => {
|
|
11
|
+
while (cursor <= last) {
|
|
12
|
+
const item = pending[cursor++];
|
|
13
|
+
const existing = visible.get(item.queueId);
|
|
14
|
+
merged.push(existing ? { ...existing, text: item.text } :
|
|
15
|
+
{ id: `pending:${item.queueId}`, kind: 'queued', ...item });
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const position = entry.kind === 'queued' && entry.queueId !== undefined
|
|
20
|
+
? positions.get(entry.queueId) : undefined;
|
|
21
|
+
if (position === undefined)
|
|
22
|
+
merged.push(entry);
|
|
23
|
+
else
|
|
24
|
+
appendThrough(position);
|
|
25
|
+
}
|
|
26
|
+
appendThrough(pending.length - 1);
|
|
27
|
+
return merged;
|
|
28
|
+
}
|
package/dist/agent/reducer.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - `agent_end` after a turn that produced no `turn_end` and no error notice
|
|
11
11
|
* synthesizes a generic error notice (a swallowed throw is never a no-op).
|
|
12
12
|
*/
|
|
13
|
+
import type { PendingSteering } from './queued-messages.js';
|
|
13
14
|
import type { AgentEvent, ModelCost, ToolResult, TokenUsage } from '@aragon-agent/core';
|
|
14
15
|
import type { AgentMode } from './agent-mode.js';
|
|
15
16
|
import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '../team/types.js';
|
|
@@ -40,7 +41,7 @@ export type Entry = {
|
|
|
40
41
|
/**
|
|
41
42
|
* A message submitted while a run was in flight (tui-shift-enter-copy-queue
|
|
42
43
|
* 5.2). It renders as `Queue: <first line>` and is rewritten IN PLACE, same
|
|
43
|
-
* id, into the `user` entry above by the
|
|
44
|
+
* id, into the `user` entry above by the matching receipt that proves Core
|
|
44
45
|
* accepted it -- so the id-based height cache re-measures rather than
|
|
45
46
|
* freezes, and the settled boundary (positional, monotonic) never moves.
|
|
46
47
|
*
|
|
@@ -51,6 +52,7 @@ export type Entry = {
|
|
|
51
52
|
id: string;
|
|
52
53
|
kind: 'queued';
|
|
53
54
|
text: string;
|
|
55
|
+
queueId?: string;
|
|
54
56
|
} | {
|
|
55
57
|
id: string;
|
|
56
58
|
kind: 'assistant';
|
|
@@ -275,6 +277,7 @@ export interface RetrySnapshot {
|
|
|
275
277
|
}
|
|
276
278
|
export interface ViewState {
|
|
277
279
|
entries: Entry[];
|
|
280
|
+
pendingSteering: PendingSteering[];
|
|
278
281
|
status: 'idle' | 'running';
|
|
279
282
|
usageTotal: UsageTotal;
|
|
280
283
|
/**
|
|
@@ -402,7 +405,11 @@ export type ViewAction = {
|
|
|
402
405
|
*/
|
|
403
406
|
| {
|
|
404
407
|
type: 'steerQueued';
|
|
408
|
+
queueId: string;
|
|
405
409
|
text: string;
|
|
410
|
+
} | {
|
|
411
|
+
type: 'steeringAccepted';
|
|
412
|
+
ids: readonly string[];
|
|
406
413
|
} | {
|
|
407
414
|
type: 'runStart';
|
|
408
415
|
} | {
|
package/dist/agent/reducer.js
CHANGED
|
@@ -46,6 +46,7 @@ const EMPTY_USAGE_TOTAL = {
|
|
|
46
46
|
export function initialViewState(seed = {}) {
|
|
47
47
|
return {
|
|
48
48
|
entries: [],
|
|
49
|
+
pendingSteering: [],
|
|
49
50
|
status: 'idle',
|
|
50
51
|
usageTotal: EMPTY_USAGE_TOTAL,
|
|
51
52
|
context: emptyContextUsage(),
|
|
@@ -127,6 +128,8 @@ function firstLine(text) {
|
|
|
127
128
|
*/
|
|
128
129
|
export function reduceEvent(event, cost, patches) {
|
|
129
130
|
switch (event.type) {
|
|
131
|
+
case 'steering_accepted':
|
|
132
|
+
return [{ type: 'steeringAccepted', ids: event.ids }];
|
|
130
133
|
case 'agent_start':
|
|
131
134
|
return [{ type: 'runStart' }];
|
|
132
135
|
case 'turn_start':
|
|
@@ -289,29 +292,6 @@ function nextId(state) {
|
|
|
289
292
|
function mapEntry(entries, id, fn) {
|
|
290
293
|
return entries.map((e) => (e.id === id ? fn(e) : e));
|
|
291
294
|
}
|
|
292
|
-
/**
|
|
293
|
-
* Rewrite every `queued` entry in place as the user message it became
|
|
294
|
-
* (tui-shift-enter-copy-queue 5.2.3).
|
|
295
|
-
*
|
|
296
|
-
* SAME ID, NEW OBJECT -- the `mapEntry` discipline for a non-append change:
|
|
297
|
-
* the position (and therefore the settled boundary, which advances by
|
|
298
|
-
* POSITION and never by identity) is untouched, while the new object
|
|
299
|
-
* reference is what lets `React.memo` and the height cache see that this
|
|
300
|
-
* one row changed. ALL of them at once, because `drainSteering` is a full
|
|
301
|
-
* drain: converting only some would show a partial acceptance that never
|
|
302
|
-
* happened. Identity-preserving when nothing is queued -- `turnStart` fires
|
|
303
|
-
* every turn, and this must stay free for the common case.
|
|
304
|
-
*/
|
|
305
|
-
function convertQueuedEntries(entries) {
|
|
306
|
-
let touched = false;
|
|
307
|
-
const next = entries.map((e) => {
|
|
308
|
-
if (e.kind !== 'queued')
|
|
309
|
-
return e;
|
|
310
|
-
touched = true;
|
|
311
|
-
return { id: e.id, kind: 'user', text: e.text };
|
|
312
|
-
});
|
|
313
|
-
return touched ? next : entries;
|
|
314
|
-
}
|
|
315
295
|
/**
|
|
316
296
|
* Seal `thinkingMs`, once, as a spreadable patch (§3.1.2).
|
|
317
297
|
*
|
|
@@ -450,12 +430,27 @@ export function viewReducer(state, action) {
|
|
|
450
430
|
}
|
|
451
431
|
case 'steerQueued': {
|
|
452
432
|
const { id, seq } = nextId(state);
|
|
453
|
-
const entry = { id, kind: 'queued', text: action.text };
|
|
433
|
+
const entry = { id, kind: 'queued', queueId: action.queueId, text: action.text };
|
|
454
434
|
// NOTHING ELSE RESETS. This is not a new turn -- `turnProduced`,
|
|
455
435
|
// `errorNoticed` and `aborted` describe the run that is still in
|
|
456
436
|
// flight, and clearing them here would let the next `runEnd` failure
|
|
457
437
|
// guard misfire on a turn that produced plenty.
|
|
458
|
-
return {
|
|
438
|
+
return {
|
|
439
|
+
...state, seq, ...appendEntry(state, state.entries, entry),
|
|
440
|
+
pendingSteering: [...state.pendingSteering, { queueId: action.queueId, text: action.text }],
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
case 'steeringAccepted': {
|
|
444
|
+
const receipts = new Set(action.ids);
|
|
445
|
+
const ids = new Set(state.pendingSteering
|
|
446
|
+
.filter((item) => receipts.has(item.queueId)).map((item) => item.queueId));
|
|
447
|
+
if (ids.size === 0)
|
|
448
|
+
return state;
|
|
449
|
+
const pendingSteering = state.pendingSteering.filter((item) => !ids.has(item.queueId));
|
|
450
|
+
const entries = state.entries.map((entry) => entry.kind === 'queued' && entry.queueId !== undefined && ids.has(entry.queueId)
|
|
451
|
+
? { id: entry.id, kind: 'user', text: entry.text }
|
|
452
|
+
: entry);
|
|
453
|
+
return { ...state, pendingSteering, entries };
|
|
459
454
|
}
|
|
460
455
|
case 'runStart':
|
|
461
456
|
return {
|
|
@@ -467,14 +462,6 @@ export function viewReducer(state, action) {
|
|
|
467
462
|
};
|
|
468
463
|
case 'turnStart': {
|
|
469
464
|
const { id, seq } = nextId(state);
|
|
470
|
-
// CONVERT, THEN APPEND, IN THAT ORDER AND IN THIS ONE ACTION
|
|
471
|
-
// (5.2.3): `turn_start` is the authoritative "the steering queue was
|
|
472
|
-
// drained into the conversation" signal (the loop drains at its top,
|
|
473
|
-
// immediately before emitting it), so the user message and the answer
|
|
474
|
-
// that follows it must land in the same commit, in render order, or a
|
|
475
|
-
// frame can exist where the message has vanished and the answer has
|
|
476
|
-
// not appeared.
|
|
477
|
-
const entries = convertQueuedEntries(state.entries);
|
|
478
465
|
const entry = {
|
|
479
466
|
id,
|
|
480
467
|
kind: 'assistant',
|
|
@@ -483,7 +470,7 @@ export function viewReducer(state, action) {
|
|
|
483
470
|
thinkingOpen: false,
|
|
484
471
|
streaming: true,
|
|
485
472
|
};
|
|
486
|
-
return { ...state, seq, ...appendEntry(state, entries, entry), streamingId: id };
|
|
473
|
+
return { ...state, seq, ...appendEntry(state, state.entries, entry), streamingId: id };
|
|
487
474
|
}
|
|
488
475
|
case 'thinkingStart': {
|
|
489
476
|
if (!state.streamingId)
|
|
@@ -777,6 +764,7 @@ export function viewReducer(state, action) {
|
|
|
777
764
|
case 'resetConversation':
|
|
778
765
|
return {
|
|
779
766
|
...state,
|
|
767
|
+
pendingSteering: [],
|
|
780
768
|
entries: [],
|
|
781
769
|
droppedEntries: 0,
|
|
782
770
|
streamingId: undefined,
|
|
@@ -1295,6 +1283,7 @@ export function viewReducer(state, action) {
|
|
|
1295
1283
|
return {
|
|
1296
1284
|
...state,
|
|
1297
1285
|
entries: restored.entries,
|
|
1286
|
+
pendingSteering: [],
|
|
1298
1287
|
droppedEntries: restored.dropped,
|
|
1299
1288
|
seq: maxSeq,
|
|
1300
1289
|
teamEntryId: undefined,
|
|
@@ -9,6 +9,7 @@ import { isAbsolute, resolve } from 'node:path';
|
|
|
9
9
|
import { resolveOutputTokens } from '@aragon-agent/core';
|
|
10
10
|
import { clampThinkingLevel, clampTheme, isAdapterProvider, isThemeName, parseMaxTokensInput, ADAPTER_PROVIDERS, DEFAULT_MAX_TOKENS, HARD_MAX_RETRIES, LEGACY_DARK_THEME, THEME_NAMES, THINKING_LEVELS, } from '../config/schema.js';
|
|
11
11
|
import { computeCost, formatCost, formatDuration, formatTokens } from '../agent/usage.js';
|
|
12
|
+
import { mergePendingEntries } from '../agent/queued-messages.js';
|
|
12
13
|
import { TODO_FOLLOW_LIMITS } from '../todo/limits.js';
|
|
13
14
|
import { buildContinuationMessage } from '../todo/follow-through.js';
|
|
14
15
|
import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
@@ -882,6 +883,10 @@ const COMMANDS = [
|
|
|
882
883
|
name: 'reset',
|
|
883
884
|
description: 'Start a new conversation',
|
|
884
885
|
run: (ctx) => {
|
|
886
|
+
if (ctx.controller.isRunning()) {
|
|
887
|
+
ctx.notify('warn', 'Cannot reset while the agent is running. Wait for it to stop.');
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
885
890
|
ctx.controller.clearMessages();
|
|
886
891
|
ctx.controller.clearAllQueues();
|
|
887
892
|
ctx.dispatch({ type: 'resetConversation' });
|
|
@@ -920,7 +925,7 @@ const COMMANDS = [
|
|
|
920
925
|
...(cfg.baseUrl ? { baseUrl: cfg.baseUrl } : {}),
|
|
921
926
|
},
|
|
922
927
|
messages: ctx.controller.getMessages(),
|
|
923
|
-
entries: ctx.state.entries,
|
|
928
|
+
entries: mergePendingEntries(ctx.state.entries, ctx.state.pendingSteering),
|
|
924
929
|
// `/resume` restores `messages`, so the belief that justifies the
|
|
925
930
|
// panel comes back — and the panel has to come back with it (§3.13).
|
|
926
931
|
todos: ctx.controller.getTodoSnapshot()?.items ?? [],
|
|
@@ -936,16 +941,22 @@ const COMMANDS = [
|
|
|
936
941
|
name: 'resume',
|
|
937
942
|
description: 'Load a saved session',
|
|
938
943
|
run: (ctx) => {
|
|
944
|
+
if (ctx.controller.isRunning()) {
|
|
945
|
+
ctx.notify('warn', 'Cannot resume while the agent is running. Wait for it to stop.');
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
939
948
|
try {
|
|
940
949
|
const path = resolveSessionPath(ctx.args, ctx.controller.getCwd());
|
|
941
950
|
const session = loadSession(path);
|
|
951
|
+
if (ctx.controller.isRunning()) {
|
|
952
|
+
ctx.notify('warn', 'Cannot resume while the agent is running. Wait for it to stop.');
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
// Loading has validated and normalized every external value. Commit
|
|
956
|
+
// synchronously so no new run can start between clearing and restoring.
|
|
957
|
+
ctx.controller.clearAllQueues();
|
|
942
958
|
ctx.controller.replaceMessages(session.messages);
|
|
943
|
-
//
|
|
944
|
-
// file fails the whole resume rather than half of it — and AN ABSENT
|
|
945
|
-
// `todos` CLEARS RATHER THAN SKIPPING (P0-2 / D-22). `replaceMessages`
|
|
946
|
-
// has just discarded the conversation the current list belonged to, so
|
|
947
|
-
// leaving the rail up produces a panel that disagrees with the model,
|
|
948
|
-
// which §1.2 ranks below having no panel at all.
|
|
959
|
+
// Absence clears the previous conversation's list, including old files.
|
|
949
960
|
ctx.controller.restoreTodos(session.todos ?? []);
|
|
950
961
|
ctx.dispatch({ type: 'restoreEntries', entries: session.entries });
|
|
951
962
|
if (session.model?.providerId && session.model?.modelId) {
|
|
@@ -1049,6 +1060,9 @@ const COMMANDS = [
|
|
|
1049
1060
|
' Windows Terminal settings.json, actions array:',
|
|
1050
1061
|
' { "keys": "shift+enter", "command": { "action": "sendInput",',
|
|
1051
1062
|
' "input": "\\u001b[13;2u" } }',
|
|
1063
|
+
' PowerShell runs inside the terminal; configure this binding in',
|
|
1064
|
+
' Windows Terminal, not in the PowerShell profile. If both keys',
|
|
1065
|
+
' send CR, the application cannot distinguish Shift+Enter from Enter.',
|
|
1052
1066
|
' VS Code terminal keybindings.json:',
|
|
1053
1067
|
' { "key": "shift+enter",',
|
|
1054
1068
|
' "command": "workbench.action.terminal.sendSequence",',
|
|
@@ -38,16 +38,17 @@ export interface EnterSequenceSpec {
|
|
|
38
38
|
* `'\r'` rather than to a newline.
|
|
39
39
|
*/
|
|
40
40
|
export declare const ENTER_SEQUENCES: readonly EnterSequenceSpec[];
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
export type EnterSequenceSegment = {
|
|
42
|
+
kind: 'text';
|
|
43
|
+
text: string;
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'newline';
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'submit';
|
|
48
|
+
};
|
|
49
|
+
/** Split explicit keys using the recognition table; bare CR/LF remain text. */
|
|
50
|
+
export declare function splitEnterSequences(text: string): EnterSequenceSegment[];
|
|
51
|
+
/** Normalize explicit Enter keys without changing unknown sequences. */
|
|
51
52
|
export declare function rewriteEnterSequences(text: string): string;
|
|
52
53
|
/**
|
|
53
54
|
* Length of the trailing run of `text` that is a proper prefix of an
|
|
@@ -30,6 +30,7 @@ import { ENTER_NEWLINE_FRAME } from './limits.js';
|
|
|
30
30
|
* `'\r'` rather than to a newline.
|
|
31
31
|
*/
|
|
32
32
|
export const ENTER_SEQUENCES = [
|
|
33
|
+
{ seq: '\x1b[13;1u', meaning: 'enter', to: '\r' },
|
|
33
34
|
{
|
|
34
35
|
seq: '\x1b[13;2u',
|
|
35
36
|
meaning: 'shift+enter',
|
|
@@ -88,34 +89,35 @@ export const ENTER_SEQUENCES = [
|
|
|
88
89
|
* up to 6 of them.
|
|
89
90
|
*/
|
|
90
91
|
const MAX_ENTER_PREFIX = Math.max(...ENTER_SEQUENCES.map((s) => s.seq.length)) - 1;
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
/** Split explicit keys using the recognition table; bare CR/LF remain text. */
|
|
93
|
+
export function splitEnterSequences(text) {
|
|
94
|
+
const segments = [];
|
|
95
|
+
let start = 0;
|
|
96
|
+
let index = 0;
|
|
97
|
+
while (index < text.length) {
|
|
98
|
+
const match = text[index] === '\x1b'
|
|
99
|
+
? ENTER_SEQUENCES.find((spec) => text.startsWith(spec.seq, index)) : undefined;
|
|
100
|
+
if (!match) {
|
|
101
|
+
index += 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (index > start)
|
|
105
|
+
segments.push({ kind: 'text', text: text.slice(start, index) });
|
|
106
|
+
segments.push({ kind: match.to === '\r' ? 'submit' : 'newline' });
|
|
107
|
+
index += match.seq.length;
|
|
108
|
+
start = index;
|
|
109
|
+
}
|
|
110
|
+
if (start < text.length)
|
|
111
|
+
segments.push({ kind: 'text', text: text.slice(start) });
|
|
112
|
+
return segments;
|
|
113
|
+
}
|
|
114
|
+
/** Normalize explicit Enter keys without changing unknown sequences. */
|
|
103
115
|
export function rewriteEnterSequences(text) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// above records in each entry's `to` field.
|
|
110
|
-
let out = text.replace(CSI_U_ENTER, (seq) => (seq.includes(';') ? ENTER_NEWLINE_FRAME : '\r'));
|
|
111
|
-
// Only the two literal Alt+Enter encodings remain. `\x1b\n` is checked
|
|
112
|
-
// before `\x1b\r` is replaced; they share no bytes, so order is free, but
|
|
113
|
-
// splitting the escapes keeps each replacement self-evident.
|
|
114
|
-
if (out.includes('\x1b\n'))
|
|
115
|
-
out = out.split('\x1b\n').join(ENTER_NEWLINE_FRAME);
|
|
116
|
-
if (out.includes('\x1b\r'))
|
|
117
|
-
out = out.split('\x1b\r').join(ENTER_NEWLINE_FRAME);
|
|
118
|
-
return out;
|
|
116
|
+
return splitEnterSequences(text).map((segment) => {
|
|
117
|
+
if (segment.kind === 'text')
|
|
118
|
+
return segment.text;
|
|
119
|
+
return segment.kind === 'submit' ? '\r' : ENTER_NEWLINE_FRAME;
|
|
120
|
+
}).join('');
|
|
119
121
|
}
|
|
120
122
|
/** True when `s` is a PROPER prefix of some recognized sequence (never whole). */
|
|
121
123
|
function isEnterPrefix(s) {
|