u-foo 3.0.8 → 3.0.10
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u-foo",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.10",
|
|
4
4
|
"description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://ufoo.dev",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@anthropic-ai/claude-agent-sdk": "^0.2.138",
|
|
48
|
-
"@openai/codex-sdk": "^0.
|
|
48
|
+
"@openai/codex-sdk": "^0.145.0",
|
|
49
49
|
"@xterm/addon-serialize": "^0.14.0",
|
|
50
50
|
"@xterm/headless": "^6.0.0",
|
|
51
51
|
"chalk": "^4.1.2",
|
|
@@ -315,7 +315,6 @@ class AgentNotifier {
|
|
|
315
315
|
if (this.stopped) return;
|
|
316
316
|
|
|
317
317
|
const currentCount = this.getMessageCount();
|
|
318
|
-
const nowMs = Date.now();
|
|
319
318
|
|
|
320
319
|
// 有新消息
|
|
321
320
|
if (currentCount > this.lastCount) {
|
|
@@ -330,14 +329,22 @@ class AgentNotifier {
|
|
|
330
329
|
}
|
|
331
330
|
|
|
332
331
|
this.lastCount = this.getMessageCount();
|
|
333
|
-
|
|
332
|
+
// Delivery moved to the daemon scheduler. ActivityDetector owns
|
|
333
|
+
// working → idle / waiting_input. Never force-idle over working here:
|
|
334
|
+
// lastWorkingAt is only set by the legacy deliverPending path, so the
|
|
335
|
+
// old hold-window check stayed permanently true and stomped real
|
|
336
|
+
// working states every poll (~2s) — Codex injections then slipped mid-turn.
|
|
337
|
+
if (this._launcherReady) {
|
|
334
338
|
const currentActivityState = this.getCurrentActivityState();
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
339
|
+
if (
|
|
340
|
+
currentActivityState
|
|
341
|
+
&& currentActivityState !== "working"
|
|
342
|
+
&& currentActivityState !== "waiting_input"
|
|
343
|
+
&& currentActivityState !== "blocked"
|
|
344
|
+
) {
|
|
345
|
+
// Soft fallback only (no force): ready/starting → idle for delivery,
|
|
346
|
+
// without overriding detector-owned busy states.
|
|
347
|
+
this.updateActivityState("idle");
|
|
341
348
|
}
|
|
342
349
|
}
|
|
343
350
|
this.refreshTitle();
|
package/src/app/chat/commands.js
CHANGED
|
@@ -190,7 +190,13 @@ function buildCommandRegistry(tree) {
|
|
|
190
190
|
.sort(sortCommands)
|
|
191
191
|
.map((cmd) => {
|
|
192
192
|
const node = tree[cmd] || {};
|
|
193
|
-
|
|
193
|
+
const entry = { cmd, ...mapNode(node) };
|
|
194
|
+
// Stamp priority so buildCompletions keeps launch → group → bus → ctx
|
|
195
|
+
// at the top of the `/` popup instead of falling back to A–Z.
|
|
196
|
+
if (COMMAND_ORDER_MAP.has(cmd)) {
|
|
197
|
+
entry.order = COMMAND_ORDER_MAP.get(cmd);
|
|
198
|
+
}
|
|
199
|
+
return entry;
|
|
194
200
|
});
|
|
195
201
|
}
|
|
196
202
|
|
|
@@ -58,6 +58,14 @@ function hasPendingUserPrompts(executionState = null) {
|
|
|
58
58
|
return state.pendingUserPrompts.length > 0;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** Peek pending nudge texts without draining (for TUI queue banner). */
|
|
62
|
+
function listPendingUserPrompts(executionState = null) {
|
|
63
|
+
const state = ensurePendingUserPrompts(executionState);
|
|
64
|
+
return state.pendingUserPrompts
|
|
65
|
+
.map((entry) => String(entry && entry.text || "").trim())
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
|
|
61
69
|
function shouldFrameAsUserReminder(executionState = null) {
|
|
62
70
|
if (!executionState || typeof executionState !== "object") return false;
|
|
63
71
|
if (executionState.planMode === true) return true;
|
|
@@ -160,6 +168,7 @@ module.exports = {
|
|
|
160
168
|
drainUserPrompts,
|
|
161
169
|
clearUserPrompts,
|
|
162
170
|
hasPendingUserPrompts,
|
|
171
|
+
listPendingUserPrompts,
|
|
163
172
|
shouldFrameAsUserReminder,
|
|
164
173
|
formatUserReminderMessage,
|
|
165
174
|
buildContinuationUserPrompt,
|
|
@@ -5,6 +5,7 @@ const Injector = require("../../coordination/bus/inject");
|
|
|
5
5
|
const { buildPromptInjectionText } = require("../../coordination/bus/promptEnvelope");
|
|
6
6
|
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
7
7
|
const { normalizeQueueEnvelope } = require("../../coordination/bus/deliveryQueue");
|
|
8
|
+
const { writeActivityState } = require("../../agents/activity/activityStateWriter");
|
|
8
9
|
|
|
9
10
|
function asState(value = "") {
|
|
10
11
|
return String(value || "").trim().toLowerCase();
|
|
@@ -58,6 +59,14 @@ class DeliveryScheduler {
|
|
|
58
59
|
this.emitDelivery = typeof options.emitDelivery === "function"
|
|
59
60
|
? options.emitDelivery
|
|
60
61
|
: async () => {};
|
|
62
|
+
this.markWorking = typeof options.markWorking === "function"
|
|
63
|
+
? options.markWorking
|
|
64
|
+
: (subscriber) => {
|
|
65
|
+
writeActivityState(this.paths.agentsFile, subscriber, "working", {
|
|
66
|
+
force: true,
|
|
67
|
+
detail: "inject",
|
|
68
|
+
});
|
|
69
|
+
};
|
|
61
70
|
this.log = typeof options.log === "function" ? options.log : () => {};
|
|
62
71
|
this.now = typeof options.now === "function" ? options.now : () => Date.now();
|
|
63
72
|
this.deferWarnAfterMs = positiveMs(options.deferWarnAfterMs, DEFAULT_DEFER_WARN_AFTER_MS);
|
|
@@ -224,6 +233,15 @@ class DeliveryScheduler {
|
|
|
224
233
|
try {
|
|
225
234
|
await this.injector.inject(subscriber, injectionText);
|
|
226
235
|
queue.completeClaim(claim);
|
|
236
|
+
// Close the idle gate immediately. PTY ActivityDetector will refresh
|
|
237
|
+
// working from output, then quiet-window back to idle; without this
|
|
238
|
+
// stamp a second pending message can slip through on the next tick
|
|
239
|
+
// before any Codex stdout arrives.
|
|
240
|
+
try {
|
|
241
|
+
this.markWorking(subscriber);
|
|
242
|
+
} catch {
|
|
243
|
+
// activity stamp must never undo a successful inject
|
|
244
|
+
}
|
|
227
245
|
await this.emitDelivery({
|
|
228
246
|
subscriber,
|
|
229
247
|
event: envelope,
|
package/src/ui/ink/ChatApp.js
CHANGED
|
@@ -732,6 +732,86 @@ function wrapInternalPlainLine(text = "", width = 80) {
|
|
|
732
732
|
return rows;
|
|
733
733
|
}
|
|
734
734
|
|
|
735
|
+
// Ink's wrap:"wrap" + <Static> under-counts CJK row height on live appends,
|
|
736
|
+
// so later log writes overpaint the previous line. Pre-wrap by display cells
|
|
737
|
+
// and paint with wrap:"truncate" so each Static item occupies a known height.
|
|
738
|
+
function expandChatLogPhysicalLines(text = "", width = 80) {
|
|
739
|
+
const limit = Math.max(1, Math.floor(Number(width) || 80));
|
|
740
|
+
const source = String(text || "").replace(/\r/g, "");
|
|
741
|
+
if (!source) return [""];
|
|
742
|
+
const out = [];
|
|
743
|
+
for (const line of source.split("\n")) {
|
|
744
|
+
out.push(...wrapInternalPlainLine(line, limit));
|
|
745
|
+
}
|
|
746
|
+
return out.length > 0 ? out : [""];
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function padDisplayCells(cells = 0) {
|
|
750
|
+
return " ".repeat(Math.max(0, Math.floor(Number(cells) || 0)));
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function splitUserLogAtMention(bodyText = "") {
|
|
754
|
+
const body = String(bodyText || "");
|
|
755
|
+
const atMatch = body.match(/^@([^\s]+)\s+(.*)$/);
|
|
756
|
+
if (atMatch) {
|
|
757
|
+
return { at: atMatch[1], rest: atMatch[2] || "" };
|
|
758
|
+
}
|
|
759
|
+
return { at: "", rest: body };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Flatten one chat log row into terminal-width physical lines. Each returned
|
|
764
|
+
* line is a single string (marker/speaker/body already merged) so Ink never
|
|
765
|
+
* has to wrap it — critical for <Static> append-only CJK safety.
|
|
766
|
+
*/
|
|
767
|
+
function buildChatLogDisplayLines(row = {}, options = {}) {
|
|
768
|
+
const cols = Math.max(8, Math.floor(Number(options.cols) || 80));
|
|
769
|
+
const continuation = Boolean(options.continuation);
|
|
770
|
+
const groupKind = options.groupKind || row.kind || "plain";
|
|
771
|
+
const kind = row.kind || "plain";
|
|
772
|
+
|
|
773
|
+
if (kind === "spacer") return [" "];
|
|
774
|
+
if (kind === "divider") {
|
|
775
|
+
return [fitPlainLine(` ${compactDividerLabel(row.body || row.bodyText || "")}`, cols)];
|
|
776
|
+
}
|
|
777
|
+
if (kind === "banner") {
|
|
778
|
+
return expandChatLogPhysicalLines(stripInternalLogMarkup(row.bodyText || row.body || ""), cols)
|
|
779
|
+
.map((line) => fitPlainLine(line, cols));
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const markerText = continuation
|
|
783
|
+
? (groupKind === "assistant" || groupKind === "agent" || groupKind === "report" ? " " : " ")
|
|
784
|
+
: String(row.markerText != null ? row.markerText : "");
|
|
785
|
+
|
|
786
|
+
if (kind === "user") {
|
|
787
|
+
const userBody = splitUserLogAtMention(row.bodyText || row.body || "");
|
|
788
|
+
const atPrefix = userBody.at ? `@${userBody.at} ` : "";
|
|
789
|
+
const firstPrefix = `${markerText || "› "}${atPrefix}`;
|
|
790
|
+
const prefixCells = fmt.displayCellWidth(firstPrefix);
|
|
791
|
+
const budget = Math.max(1, cols - prefixCells);
|
|
792
|
+
const chunks = expandChatLogPhysicalLines(userBody.rest, budget);
|
|
793
|
+
const contPad = padDisplayCells(prefixCells);
|
|
794
|
+
return chunks.map((chunk, idx) => {
|
|
795
|
+
const line = idx === 0 ? `${firstPrefix}${chunk}` : `${contPad}${chunk}`;
|
|
796
|
+
return fitPlainLine(line, cols);
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const speakerPrefix = (!continuation && row.speaker)
|
|
801
|
+
? `${row.speaker} · `
|
|
802
|
+
: "";
|
|
803
|
+
const head = `${markerText}${speakerPrefix}`;
|
|
804
|
+
const headCells = fmt.displayCellWidth(head);
|
|
805
|
+
const budget = Math.max(1, cols - headCells);
|
|
806
|
+
const bodyPlain = stripInternalLogMarkup(row.bodyText != null ? row.bodyText : (row.body || ""));
|
|
807
|
+
const chunks = expandChatLogPhysicalLines(bodyPlain, budget);
|
|
808
|
+
const contPad = padDisplayCells(headCells);
|
|
809
|
+
return chunks.map((chunk, idx) => {
|
|
810
|
+
const line = idx === 0 ? `${head}${chunk}` : `${contPad}${chunk}`;
|
|
811
|
+
return fitPlainLine(line, cols);
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
735
815
|
function classifyInternalLogLine(line = "") {
|
|
736
816
|
const raw = stripInternalLogMarkup(line).replace(/\r/g, "");
|
|
737
817
|
if (!raw) return { kind: "spacer", text: "", markdown: false, bold: false };
|
|
@@ -3580,16 +3660,44 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3580
3660
|
return null;
|
|
3581
3661
|
}
|
|
3582
3662
|
|
|
3583
|
-
const
|
|
3584
|
-
const
|
|
3585
|
-
const
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3663
|
+
const renderChatLogLines = (row, { key, continuation = false, groupKind = "", marginTop = 0, marginBottom = 0 } = {}) => {
|
|
3664
|
+
const colors = CHAT_LOG_ROW_PALETTE[row.kind] || CHAT_LOG_ROW_PALETTE.plain;
|
|
3665
|
+
const cols = Math.max(20, size.cols || 80);
|
|
3666
|
+
const lines = buildChatLogDisplayLines(row, {
|
|
3667
|
+
continuation,
|
|
3668
|
+
groupKind: groupKind || row.kind,
|
|
3669
|
+
cols,
|
|
3670
|
+
});
|
|
3671
|
+
const textProps = {
|
|
3672
|
+
color: row.kind === "user" ? "green" : colors.body,
|
|
3673
|
+
bold: Boolean(
|
|
3674
|
+
row.kind === "user"
|
|
3675
|
+
|| colors.bold
|
|
3676
|
+
|| row.kind === "error"
|
|
3677
|
+
|| row.kind === "assistant"
|
|
3678
|
+
|| row.kind === "banner"
|
|
3679
|
+
),
|
|
3680
|
+
wrap: "truncate",
|
|
3681
|
+
};
|
|
3682
|
+
if (colors.dim) textProps.dimColor = true;
|
|
3683
|
+
if (row.kind === "agent" || row.kind === "report") {
|
|
3684
|
+
textProps.color = colors.speaker;
|
|
3591
3685
|
}
|
|
3592
|
-
|
|
3686
|
+
if (lines.length <= 1) {
|
|
3687
|
+
return h(Box, { key, width: "100%", marginTop, marginBottom },
|
|
3688
|
+
h(Text, textProps, (lines[0] != null ? lines[0] : " ") || " "));
|
|
3689
|
+
}
|
|
3690
|
+
return h(Box, {
|
|
3691
|
+
key,
|
|
3692
|
+
flexDirection: "column",
|
|
3693
|
+
width: "100%",
|
|
3694
|
+
marginTop,
|
|
3695
|
+
marginBottom,
|
|
3696
|
+
},
|
|
3697
|
+
...lines.map((line, idx) => h(Text, {
|
|
3698
|
+
key: `${key}-r${idx}`,
|
|
3699
|
+
...textProps,
|
|
3700
|
+
}, line || " ")));
|
|
3593
3701
|
};
|
|
3594
3702
|
|
|
3595
3703
|
const renderChatLogEntry = (entry, group) => {
|
|
@@ -3598,53 +3706,12 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3598
3706
|
if (row.kind === "spacer") {
|
|
3599
3707
|
return h(Text, { key, color: "gray" }, " ");
|
|
3600
3708
|
}
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
}
|
|
3607
|
-
if (row.kind === "banner") {
|
|
3608
|
-
return h(Box, { key },
|
|
3609
|
-
h(Text, { color: colors.body, bold: true, wrap: "truncate" }, row.body),
|
|
3610
|
-
);
|
|
3611
|
-
}
|
|
3612
|
-
if (row.kind === "user") {
|
|
3613
|
-
const userBody = renderUserLogBody(row.bodyText);
|
|
3614
|
-
return h(Box, { key, width: "100%", marginBottom: 1, alignItems: "flex-start" },
|
|
3615
|
-
h(Text, { color: "green", bold: true }, row.markerText || "› "),
|
|
3616
|
-
userBody.at
|
|
3617
|
-
? h(Text, { color: "magenta", bold: true }, `@${userBody.at} `)
|
|
3618
|
-
: null,
|
|
3619
|
-
h(Text, { color: "green", bold: true, wrap: "wrap" }, userBody.rest),
|
|
3620
|
-
);
|
|
3621
|
-
}
|
|
3622
|
-
const markerText = entry && entry.continuation
|
|
3623
|
-
? (group && (group.kind === "assistant" || group.kind === "agent" || group.kind === "report") ? " " : " ")
|
|
3624
|
-
: row.markerText;
|
|
3625
|
-
const bodyProps = {
|
|
3626
|
-
color: colors.body,
|
|
3627
|
-
wrap: "wrap",
|
|
3628
|
-
};
|
|
3629
|
-
if (colors.dim) bodyProps.dimColor = true;
|
|
3630
|
-
// Pin the gutter glyph to the first text line; default Yoga stretch/center
|
|
3631
|
-
// floats markers above wrapped speaker · body rows.
|
|
3632
|
-
return h(Box, { key, width: "100%", alignItems: "flex-start" },
|
|
3633
|
-
h(Text, {
|
|
3634
|
-
color: colors.marker,
|
|
3635
|
-
bold: row.kind === "error" || row.kind === "assistant",
|
|
3636
|
-
dimColor: Boolean(colors.dim),
|
|
3637
|
-
}, markerText),
|
|
3638
|
-
h(Text, bodyProps,
|
|
3639
|
-
row.speaker && !(entry && entry.continuation)
|
|
3640
|
-
? h(Text, { color: colors.speaker, bold: colors.bold }, row.speaker)
|
|
3641
|
-
: null,
|
|
3642
|
-
row.speaker && !(entry && entry.continuation)
|
|
3643
|
-
? h(Text, { color: "gray" }, " · ")
|
|
3644
|
-
: null,
|
|
3645
|
-
row.bodyText,
|
|
3646
|
-
),
|
|
3647
|
-
);
|
|
3709
|
+
return renderChatLogLines(row, {
|
|
3710
|
+
key,
|
|
3711
|
+
continuation: Boolean(entry && entry.continuation),
|
|
3712
|
+
groupKind: group && group.kind ? group.kind : row.kind,
|
|
3713
|
+
marginBottom: row.kind === "user" || row.kind === "divider" ? 1 : 0,
|
|
3714
|
+
});
|
|
3648
3715
|
};
|
|
3649
3716
|
|
|
3650
3717
|
const renderChatLogGroup = (group) => {
|
|
@@ -3664,64 +3731,23 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3664
3731
|
...entries.map((entry) => renderChatLogEntry(entry, group)));
|
|
3665
3732
|
};
|
|
3666
3733
|
|
|
3667
|
-
// Renderer for one finalized (append-only) <Static> log item.
|
|
3668
|
-
//
|
|
3669
|
-
//
|
|
3670
|
-
// decoration pass flags `marginBefore` on whatever entry follows a
|
|
3671
|
-
// transcript group.
|
|
3734
|
+
// Renderer for one finalized (append-only) <Static> log item. Spacing
|
|
3735
|
+
// uses decorateStaticLogEntry's marginBefore; body text is pre-wrapped to
|
|
3736
|
+
// the terminal width so Ink never wrap:"wrap"s CJK inside Static.
|
|
3672
3737
|
const renderStaticChatLogItem = (item) => {
|
|
3673
3738
|
const { row, groupKind, continuation, marginBefore } = item;
|
|
3674
3739
|
const key = item.entry && item.entry.id ? item.entry.id : `log-${row.body}`;
|
|
3675
|
-
const marginTop = marginBefore ? 1 : 0;
|
|
3676
3740
|
if (row.kind === "spacer") {
|
|
3677
|
-
return h(Box, { key, marginTop },
|
|
3741
|
+
return h(Box, { key, marginTop: marginBefore ? 1 : 0 },
|
|
3678
3742
|
h(Text, { color: "gray" }, " "));
|
|
3679
3743
|
}
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
return h(Box, { key, marginTop },
|
|
3688
|
-
h(Text, { color: colors.body, bold: true, wrap: "truncate" }, row.body),
|
|
3689
|
-
);
|
|
3690
|
-
}
|
|
3691
|
-
if (row.kind === "user") {
|
|
3692
|
-
const userBody = renderUserLogBody(row.bodyText);
|
|
3693
|
-
return h(Box, { key, width: "100%", marginTop, marginBottom: 1, alignItems: "flex-start" },
|
|
3694
|
-
h(Text, { color: "green", bold: true }, row.markerText || "› "),
|
|
3695
|
-
userBody.at
|
|
3696
|
-
? h(Text, { color: "magenta", bold: true }, `@${userBody.at} `)
|
|
3697
|
-
: null,
|
|
3698
|
-
h(Text, { color: "green", bold: true, wrap: "wrap" }, userBody.rest),
|
|
3699
|
-
);
|
|
3700
|
-
}
|
|
3701
|
-
const markerText = continuation
|
|
3702
|
-
? (groupKind === "assistant" || groupKind === "agent" || groupKind === "report" ? " " : " ")
|
|
3703
|
-
: row.markerText;
|
|
3704
|
-
const bodyProps = {
|
|
3705
|
-
color: colors.body,
|
|
3706
|
-
wrap: "wrap",
|
|
3707
|
-
};
|
|
3708
|
-
if (colors.dim) bodyProps.dimColor = true;
|
|
3709
|
-
return h(Box, { key, width: "100%", marginTop, alignItems: "flex-start" },
|
|
3710
|
-
h(Text, {
|
|
3711
|
-
color: colors.marker,
|
|
3712
|
-
bold: row.kind === "error" || row.kind === "assistant",
|
|
3713
|
-
dimColor: Boolean(colors.dim),
|
|
3714
|
-
}, markerText),
|
|
3715
|
-
h(Text, bodyProps,
|
|
3716
|
-
row.speaker && !continuation
|
|
3717
|
-
? h(Text, { color: colors.speaker, bold: colors.bold }, row.speaker)
|
|
3718
|
-
: null,
|
|
3719
|
-
row.speaker && !continuation
|
|
3720
|
-
? h(Text, { color: "gray" }, " · ")
|
|
3721
|
-
: null,
|
|
3722
|
-
row.bodyText,
|
|
3723
|
-
),
|
|
3724
|
-
);
|
|
3744
|
+
return renderChatLogLines(row, {
|
|
3745
|
+
key,
|
|
3746
|
+
continuation,
|
|
3747
|
+
groupKind,
|
|
3748
|
+
marginTop: marginBefore ? 1 : 0,
|
|
3749
|
+
marginBottom: row.kind === "user" || row.kind === "divider" ? 1 : 0,
|
|
3750
|
+
});
|
|
3725
3751
|
};
|
|
3726
3752
|
|
|
3727
3753
|
if (state.viewingAgentId) {
|
|
@@ -3826,13 +3852,24 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3826
3852
|
const start = Math.min(completionWindowStart, Math.max(0, completions.length - POPUP_PAGE_SIZE));
|
|
3827
3853
|
const end = Math.min(completions.length, start + POPUP_PAGE_SIZE);
|
|
3828
3854
|
const visible = completions.slice(start, end);
|
|
3829
|
-
|
|
3830
|
-
|
|
3855
|
+
const cols = Math.max(8, size.cols || 80);
|
|
3856
|
+
return h(Box, { flexDirection: "column", width: "100%" },
|
|
3857
|
+
h(Text, { color: "gray" }, "─".repeat(cols)),
|
|
3831
3858
|
...visible.map((s, idxInWindow) => {
|
|
3832
3859
|
const idx = start + idxInWindow;
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3860
|
+
const selected = idx === completionIndex;
|
|
3861
|
+
// Keep label+description in one Text with wrap:"truncate".
|
|
3862
|
+
// Sibling Text nodes let Yoga shrink the label mid-command
|
|
3863
|
+
// (e.g. "/group run" / "build-lane" on two lines).
|
|
3864
|
+
const line = s.description
|
|
3865
|
+
? `${s.label} ${s.description}`
|
|
3866
|
+
: String(s.label || "");
|
|
3867
|
+
return h(Box, { key: `cmp-${idx}`, width: "100%" },
|
|
3868
|
+
h(Text, {
|
|
3869
|
+
color: selected ? "cyan" : "gray",
|
|
3870
|
+
inverse: selected,
|
|
3871
|
+
wrap: "truncate",
|
|
3872
|
+
}, line),
|
|
3836
3873
|
);
|
|
3837
3874
|
}),
|
|
3838
3875
|
);
|
|
@@ -4099,6 +4136,8 @@ module.exports = {
|
|
|
4099
4136
|
createInkStreamState,
|
|
4100
4137
|
createThrottledSender,
|
|
4101
4138
|
decorateStaticLogEntry,
|
|
4139
|
+
buildChatLogDisplayLines,
|
|
4140
|
+
expandChatLogPhysicalLines,
|
|
4102
4141
|
bootstrapEnvironment,
|
|
4103
4142
|
buildDirectBusSendRequest,
|
|
4104
4143
|
buildPromptIpcRequest,
|
package/src/ui/ink/UcodeApp.js
CHANGED
|
@@ -96,6 +96,10 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
96
96
|
hash: "",
|
|
97
97
|
}));
|
|
98
98
|
const [interactionLines, setInteractionLines] = useState([]);
|
|
99
|
+
// Bumps when pendingUserPrompts change so the near-input queue banner
|
|
100
|
+
// re-renders without dumping a chat-log system line.
|
|
101
|
+
const [queueTick, setQueueTick] = useState(0);
|
|
102
|
+
const bumpQueue = useCallback(() => setQueueTick((n) => n + 1), []);
|
|
99
103
|
const [spinnerTick, setSpinnerTick] = useState(0);
|
|
100
104
|
const [size, setSize] = useState({ cols: 0, rows: 0 });
|
|
101
105
|
const [contextMeter, setContextMeter] = useState(() => {
|
|
@@ -903,6 +907,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
903
907
|
cancelThinkingFlush();
|
|
904
908
|
thinkingTailRef.current = "";
|
|
905
909
|
refreshPlanUi();
|
|
910
|
+
bumpQueue();
|
|
906
911
|
setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
|
|
907
912
|
}
|
|
908
913
|
if (streamBuf) {
|
|
@@ -942,7 +947,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
942
947
|
default:
|
|
943
948
|
if (result.output) appendLogText(result.output);
|
|
944
949
|
}
|
|
945
|
-
}, [appendLogLine, appendLogText, exit, props, logToolHint, flushActiveMerge, flushTableBuffer, refreshPlanUi]);
|
|
950
|
+
}, [appendLogLine, appendLogText, exit, props, logToolHint, flushActiveMerge, flushTableBuffer, refreshPlanUi, bumpQueue]);
|
|
946
951
|
// ^ `props` is captured by the createUcodeApp closure on a single mount,
|
|
947
952
|
// so its reference is stable across renders even though it looks like a
|
|
948
953
|
// changing dep to React's exhaustive-deps lint.
|
|
@@ -1153,14 +1158,8 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1153
1158
|
if (!props.state.executionState || typeof props.state.executionState !== "object") {
|
|
1154
1159
|
props.state.executionState = emptyExecutionState();
|
|
1155
1160
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
appendLogText(
|
|
1159
|
-
queued.enqueued
|
|
1160
|
-
? `Queued user reminder for next model turn: ${reminderPreview}`
|
|
1161
|
-
: "Could not queue user reminder (empty).",
|
|
1162
|
-
"system",
|
|
1163
|
-
);
|
|
1161
|
+
enqueueUserPrompt(props.state.executionState, modelText);
|
|
1162
|
+
bumpQueue();
|
|
1164
1163
|
return;
|
|
1165
1164
|
}
|
|
1166
1165
|
|
|
@@ -1180,6 +1179,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1180
1179
|
appendLogLine,
|
|
1181
1180
|
flushActiveMerge,
|
|
1182
1181
|
flushTableBuffer,
|
|
1182
|
+
bumpQueue,
|
|
1183
1183
|
props.state,
|
|
1184
1184
|
props.submitUserInteractionAnswer,
|
|
1185
1185
|
refreshPlanUi,
|
|
@@ -1410,6 +1410,25 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1410
1410
|
),
|
|
1411
1411
|
)
|
|
1412
1412
|
: null,
|
|
1413
|
+
(() => {
|
|
1414
|
+
void queueTick;
|
|
1415
|
+
let pending = [];
|
|
1416
|
+
try {
|
|
1417
|
+
const { listPendingUserPrompts } = require("../../code/context/userNudge");
|
|
1418
|
+
pending = listPendingUserPrompts(props.state && props.state.executionState);
|
|
1419
|
+
} catch {
|
|
1420
|
+
pending = [];
|
|
1421
|
+
}
|
|
1422
|
+
if (pending.length === 0) return null;
|
|
1423
|
+
const latest = String(pending[pending.length - 1] || "");
|
|
1424
|
+
const more = pending.length > 1 ? ` · +${pending.length - 1}` : "";
|
|
1425
|
+
const preview = latest.length > 72 ? `${latest.slice(0, 72)}…` : latest;
|
|
1426
|
+
return h(Box, { width: "100%" },
|
|
1427
|
+
h(Text, { color: "yellow", wrap: "truncate" },
|
|
1428
|
+
`排队中 · 未发出 · ${preview}${more}`,
|
|
1429
|
+
),
|
|
1430
|
+
);
|
|
1431
|
+
})(),
|
|
1413
1432
|
h(Box, { width: "100%" },
|
|
1414
1433
|
h(MultilineInput, {
|
|
1415
1434
|
value: draft,
|
|
@@ -1469,6 +1488,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1469
1488
|
if (props.state && props.state.executionState) {
|
|
1470
1489
|
clearUserPrompts(props.state.executionState);
|
|
1471
1490
|
}
|
|
1491
|
+
bumpQueue();
|
|
1472
1492
|
} catch { /* ignore */ }
|
|
1473
1493
|
appendLogLine("⚙ Cancellation requested. Stopping the current task...", "system");
|
|
1474
1494
|
setStatus({
|