u-foo 3.0.7 → 3.0.9
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 +1 -1
- package/src/agents/launch/notifier.js +15 -8
- package/src/code/context/executionSegment.js +1 -0
- package/src/code/context/planGraphService.js +261 -55
- package/src/code/context/promptLayers.js +1 -0
- package/src/code/context/userNudge.js +22 -9
- package/src/runtime/daemon/deliveryScheduler.js +18 -0
- package/src/ui/ink/ChatApp.js +136 -108
- package/src/ui/ink/UcodeApp.js +41 -9
package/package.json
CHANGED
|
@@ -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();
|
|
@@ -27,6 +27,7 @@ function emptyExecutionState() {
|
|
|
27
27
|
pendingUserPrompts: [],
|
|
28
28
|
planGraph: require("./planGraphService").emptyPlanGraphState(),
|
|
29
29
|
graphs: {},
|
|
30
|
+
archivedPlans: [],
|
|
30
31
|
taskRuns: require("../runtime/taskRun").emptyTaskRunStore(),
|
|
31
32
|
agentMailbox: require("../runtime/loopMailbox").emptyMailbox(),
|
|
32
33
|
taskMailboxes: {},
|
|
@@ -48,6 +48,102 @@ function cloneJson(value) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
const MAX_ARCHIVED_PLANS = 5;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Snapshot a finished/replaced plan out of the live primary slot so the next
|
|
55
|
+
* create starts clean instead of overlaying the previous graph.
|
|
56
|
+
*/
|
|
57
|
+
function archivePlanGraph(executionState = null, planGraph = null, {
|
|
58
|
+
reason = "",
|
|
59
|
+
} = {}) {
|
|
60
|
+
if (!executionState || typeof executionState !== "object") return null;
|
|
61
|
+
const live = planGraph && typeof planGraph === "object" ? planGraph : null;
|
|
62
|
+
const graphId = String((live && live.graphId) || "").trim();
|
|
63
|
+
if (!live || !graphId) return null;
|
|
64
|
+
|
|
65
|
+
if (!Array.isArray(executionState.archivedPlans)) {
|
|
66
|
+
executionState.archivedPlans = [];
|
|
67
|
+
}
|
|
68
|
+
const archived = {
|
|
69
|
+
...cloneJson(live),
|
|
70
|
+
archivedAt: new Date().toISOString(),
|
|
71
|
+
archiveReason: String(reason || "").trim() || "archived",
|
|
72
|
+
};
|
|
73
|
+
executionState.archivedPlans.push(archived);
|
|
74
|
+
if (executionState.archivedPlans.length > MAX_ARCHIVED_PLANS) {
|
|
75
|
+
executionState.archivedPlans = executionState.archivedPlans.slice(-MAX_ARCHIVED_PLANS);
|
|
76
|
+
}
|
|
77
|
+
if (executionState.graphs && typeof executionState.graphs === "object") {
|
|
78
|
+
delete executionState.graphs[graphId];
|
|
79
|
+
}
|
|
80
|
+
return archived;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildPlanCompletionSummary(planGraph = null, advance = null) {
|
|
84
|
+
const pg = planGraph && typeof planGraph === "object" ? planGraph : {};
|
|
85
|
+
const nodes = Array.isArray(pg.nodes) ? pg.nodes : [];
|
|
86
|
+
const tasks = nodes.filter((node) => node && (node.type === "task" || !node.generated));
|
|
87
|
+
const succeeded = tasks.filter((n) => n.status === "succeeded").length;
|
|
88
|
+
const failed = tasks.filter((n) => (
|
|
89
|
+
n.status === "failed" || n.status === "blocked" || n.status === "cancelled"
|
|
90
|
+
)).length;
|
|
91
|
+
const lines = [
|
|
92
|
+
`Plan completed${pg.graphId ? ` (${pg.graphId})` : ""}.`,
|
|
93
|
+
];
|
|
94
|
+
if (pg.objective) lines.push(`Objective: ${String(pg.objective).slice(0, 240)}`);
|
|
95
|
+
lines.push(`Tasks: ${succeeded} succeeded, ${failed} failed/blocked, ${tasks.length} total.`);
|
|
96
|
+
for (const node of tasks.slice(0, 12)) {
|
|
97
|
+
const bit = (node.result && node.result.summary)
|
|
98
|
+
|| node.error
|
|
99
|
+
|| node.status
|
|
100
|
+
|| "";
|
|
101
|
+
lines.push(
|
|
102
|
+
`- ${node.id} [${node.status || "pending"}]`
|
|
103
|
+
+ (bit ? `: ${String(bit).slice(0, 160)}` : ""),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const executed = advance && Array.isArray(advance.executedNodes) ? advance.executedNodes : [];
|
|
107
|
+
if (executed.length > 0 && tasks.length === 0) {
|
|
108
|
+
for (const entry of executed.slice(0, 8)) {
|
|
109
|
+
lines.push(`- ${entry.id}: ${entry.summary || entry.status || "done"}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
lines.push("Summarize the outcome for the user. The active plan has been cleared; use plan_graph create for a new objective.");
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* On graph_terminal for the primary Agent Loop plan: archive, clear primary
|
|
118
|
+
* slot, and exit auto Plan Mode. Child TaskLoop graphs stay in graphs[].
|
|
119
|
+
*/
|
|
120
|
+
function finalizeTerminalPrimaryPlan(executionState = null, planGraph = null, advance = null) {
|
|
121
|
+
const pg = planGraph && typeof planGraph === "object" ? planGraph : null;
|
|
122
|
+
if (!pg || !pg.graphId) return null;
|
|
123
|
+
if (!advance || String(advance.yieldReason || "") !== "graph_terminal") return null;
|
|
124
|
+
|
|
125
|
+
const completionSummary = buildPlanCompletionSummary(pg, advance);
|
|
126
|
+
const archived = archivePlanGraph(executionState, pg, { reason: "graph_terminal" });
|
|
127
|
+
executionState.planGraph = emptyPlanGraphState();
|
|
128
|
+
executionState.mode = "single_action";
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const { getPlanModeSource, setPlanMode } = require("./planMode");
|
|
132
|
+
if (getPlanModeSource(executionState) === "auto") {
|
|
133
|
+
setPlanMode(executionState, false, { reason: "plan completed" });
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// planMode is optional for pure unit callers
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
planCompleted: true,
|
|
141
|
+
archivedGraphId: (archived && archived.graphId) || pg.graphId,
|
|
142
|
+
completionSummary,
|
|
143
|
+
planMode: Boolean(executionState.planMode),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
51
147
|
function ensurePlanGraphState(executionState = null) {
|
|
52
148
|
const state = executionState && typeof executionState === "object"
|
|
53
149
|
? executionState
|
|
@@ -614,42 +710,73 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
|
|
|
614
710
|
}
|
|
615
711
|
if (advanced.advance) advance = advanced.advance;
|
|
616
712
|
}
|
|
617
|
-
const
|
|
618
|
-
const
|
|
713
|
+
const completedGraph = executionState.planGraph;
|
|
714
|
+
const terminal = selected.isPrimary
|
|
715
|
+
? finalizeTerminalPrimaryPlan(executionState, completedGraph, advance)
|
|
716
|
+
: null;
|
|
717
|
+
const live = terminal
|
|
718
|
+
? completedGraph
|
|
719
|
+
: executionState.planGraph;
|
|
720
|
+
const { readyNodes, waitingNodes } = summarizeReadyWaiting(
|
|
721
|
+
terminal ? [] : (live.nodes || []),
|
|
722
|
+
);
|
|
619
723
|
const payload = controlResult.ok
|
|
620
724
|
? accepted({
|
|
621
|
-
graphId: live.graphId || "",
|
|
622
|
-
commandRevision: Number(live.specRevision) || 0,
|
|
623
|
-
stateRevision: Number(live.stateRevision) || 0,
|
|
624
|
-
revision: Number(live.specRevision) || 0,
|
|
725
|
+
graphId: terminal ? "" : (live.graphId || ""),
|
|
726
|
+
commandRevision: terminal ? 0 : (Number(live.specRevision) || 0),
|
|
727
|
+
stateRevision: terminal ? 0 : (Number(live.stateRevision) || 0),
|
|
728
|
+
revision: terminal ? 0 : (Number(live.specRevision) || 0),
|
|
625
729
|
control: controlResult,
|
|
626
|
-
summary:
|
|
627
|
-
?
|
|
628
|
-
: (advance.
|
|
629
|
-
|
|
730
|
+
summary: terminal
|
|
731
|
+
? terminal.completionSummary
|
|
732
|
+
: (advance.waitingFor
|
|
733
|
+
? `waiting on ${advance.waitingFor.type}:${advance.waitingFor.id || ""}`
|
|
734
|
+
: (advance.yieldReason || "control actions applied")),
|
|
735
|
+
changes: {
|
|
736
|
+
nodesAdded: [],
|
|
737
|
+
nodesUpdated: [],
|
|
738
|
+
...(terminal ? { archivedGraphId: terminal.archivedGraphId } : {}),
|
|
739
|
+
},
|
|
630
740
|
nodesAdded: [],
|
|
631
741
|
nodesUpdated: [],
|
|
632
742
|
readyNodes,
|
|
633
743
|
waitingNodes,
|
|
634
|
-
waitingFor: live.waitingFor || null,
|
|
744
|
+
waitingFor: terminal ? null : (live.waitingFor || null),
|
|
635
745
|
advance,
|
|
636
|
-
planView: projectPlanView(live),
|
|
746
|
+
planView: terminal ? [] : projectPlanView(live),
|
|
637
747
|
validationWarnings: [],
|
|
748
|
+
...(terminal ? {
|
|
749
|
+
planCompleted: true,
|
|
750
|
+
archivedGraphId: terminal.archivedGraphId,
|
|
751
|
+
completionSummary: terminal.completionSummary,
|
|
752
|
+
} : {}),
|
|
638
753
|
})
|
|
639
754
|
: rejected(controlResult.errors || [{
|
|
640
755
|
code: "CONTROL_REJECTED",
|
|
641
756
|
message: "one or more control actions rejected",
|
|
642
757
|
}], { control: controlResult });
|
|
643
|
-
if (commandId && payload.status === "accepted") {
|
|
758
|
+
if (commandId && payload.status === "accepted" && !terminal) {
|
|
644
759
|
cacheCommand(executionState.planGraph, commandId, payload);
|
|
645
760
|
}
|
|
761
|
+
restorePrimaryGraph();
|
|
646
762
|
return { ...payload, executionState, modelPayload: payload, ok: payload.status === "accepted" };
|
|
647
763
|
}
|
|
648
764
|
|
|
649
765
|
if (operation === "cancel_graph" || operation === "clear") {
|
|
650
766
|
const previousId = planGraph.graphId || "";
|
|
767
|
+
if (previousId) {
|
|
768
|
+
archivePlanGraph(executionState, planGraph, { reason: "cancel_graph" });
|
|
769
|
+
}
|
|
651
770
|
executionState.planGraph = emptyPlanGraphState();
|
|
652
771
|
executionState.mode = "single_action";
|
|
772
|
+
try {
|
|
773
|
+
const { getPlanModeSource, setPlanMode } = require("./planMode");
|
|
774
|
+
if (getPlanModeSource(executionState) === "auto") {
|
|
775
|
+
setPlanMode(executionState, false, { reason: "plan cancelled" });
|
|
776
|
+
}
|
|
777
|
+
} catch {
|
|
778
|
+
// ignore
|
|
779
|
+
}
|
|
653
780
|
const payload = accepted({
|
|
654
781
|
graphId: "",
|
|
655
782
|
commandRevision: 0,
|
|
@@ -662,6 +789,9 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
|
|
|
662
789
|
waitingNodes: [],
|
|
663
790
|
advance: { status: "completed", yieldReason: "cancelled", executedNodes: [], failedNodes: [] },
|
|
664
791
|
validationWarnings: [],
|
|
792
|
+
summary: previousId
|
|
793
|
+
? `Plan ${previousId} cancelled and cleared.`
|
|
794
|
+
: "No active plan to cancel.",
|
|
665
795
|
});
|
|
666
796
|
restorePrimaryGraph();
|
|
667
797
|
return { ...payload, executionState, modelPayload: payload };
|
|
@@ -697,15 +827,22 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
|
|
|
697
827
|
return rejectWorking(payload);
|
|
698
828
|
}
|
|
699
829
|
|
|
700
|
-
const beforeIds =
|
|
830
|
+
const beforeIds = operation === "create"
|
|
831
|
+
? new Set()
|
|
832
|
+
: new Set(listNodeIds(planGraph.nodes));
|
|
701
833
|
let nextPlan = {
|
|
702
834
|
id: planGraph.graphId || createPlanId("plan"),
|
|
703
835
|
objective: planGraph.objective || "",
|
|
704
836
|
nodes: snapshotNodes(planGraph),
|
|
705
837
|
};
|
|
706
838
|
let nodesUpdated = [];
|
|
839
|
+
let replacedGraphId = "";
|
|
707
840
|
|
|
708
841
|
if (operation === "create") {
|
|
842
|
+
if (selected.isPrimary && planGraph.graphId) {
|
|
843
|
+
replacedGraphId = String(planGraph.graphId);
|
|
844
|
+
archivePlanGraph(executionState, planGraph, { reason: "replaced_by_create" });
|
|
845
|
+
}
|
|
709
846
|
const graphSource = command.graph || {};
|
|
710
847
|
if (Array.isArray(graphSource.nodes)) {
|
|
711
848
|
nextPlan = normalizePlanGraph({
|
|
@@ -717,6 +854,11 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
|
|
|
717
854
|
nextPlan.nodes = stripModelStatuses(nextPlan.nodes);
|
|
718
855
|
}
|
|
719
856
|
if (!nextPlan.id) nextPlan.id = createPlanId("plan");
|
|
857
|
+
// Prefer a fresh id when replacing a live plan so the new graph does not
|
|
858
|
+
// collide with the archived one in graphs[].
|
|
859
|
+
if (replacedGraphId && nextPlan.id === replacedGraphId) {
|
|
860
|
+
nextPlan.id = createPlanId("plan");
|
|
861
|
+
}
|
|
720
862
|
nodesUpdated = listNodeIds(nextPlan.nodes);
|
|
721
863
|
// Parent graphs are owned by the agent loop.
|
|
722
864
|
const { agentLoopOwner } = require("../runtime/graphOwner");
|
|
@@ -798,9 +940,10 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
|
|
|
798
940
|
}
|
|
799
941
|
|
|
800
942
|
// Prefer the patched node list (includes aggregate expand status).
|
|
943
|
+
// create starts clean — do not inherit statuses from a replaced graph.
|
|
801
944
|
const preferredNodes = applyStatusesFromStore(
|
|
802
945
|
(Array.isArray(nextPlan.nodes) ? nextPlan.nodes : []).map((node) => normalizePlanNode(node, node.id)),
|
|
803
|
-
planGraph.nodes,
|
|
946
|
+
operation === "create" ? [] : planGraph.nodes,
|
|
804
947
|
{ preferSourceStatus: operation === "patch" },
|
|
805
948
|
);
|
|
806
949
|
|
|
@@ -831,28 +974,53 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
|
|
|
831
974
|
}
|
|
832
975
|
}
|
|
833
976
|
|
|
834
|
-
const specRevision =
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
||
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
977
|
+
const specRevision = operation === "create"
|
|
978
|
+
? 1
|
|
979
|
+
: (Number(planGraph.specRevision) || 0) + 1;
|
|
980
|
+
if (operation === "create") {
|
|
981
|
+
executionState.planGraph = {
|
|
982
|
+
...emptyPlanGraphState(),
|
|
983
|
+
graphId: preferredCompile.planId || nextPlan.id,
|
|
984
|
+
specRevision,
|
|
985
|
+
stateRevision: 0,
|
|
986
|
+
revision: specRevision,
|
|
987
|
+
objective: preferredCompile.objective || nextPlan.objective || "",
|
|
988
|
+
failurePolicy: preferredCompile.failurePolicy
|
|
989
|
+
|| nextPlan.failurePolicy
|
|
990
|
+
|| "continue_independent",
|
|
991
|
+
nodes: mergedNodes,
|
|
992
|
+
outputs: {},
|
|
993
|
+
waitingFor: null,
|
|
994
|
+
lastStoppedAt: "",
|
|
995
|
+
lastYieldReason: "",
|
|
996
|
+
commandLog: {},
|
|
997
|
+
owner: nextPlan.owner || null,
|
|
998
|
+
parentGraphId: "",
|
|
999
|
+
parentNodeId: "",
|
|
1000
|
+
};
|
|
1001
|
+
} else {
|
|
1002
|
+
executionState.planGraph = {
|
|
1003
|
+
...planGraph,
|
|
1004
|
+
graphId: preferredCompile.planId || nextPlan.id,
|
|
1005
|
+
specRevision,
|
|
1006
|
+
stateRevision: Number(planGraph.stateRevision) || 0,
|
|
1007
|
+
revision: specRevision,
|
|
1008
|
+
objective: preferredCompile.objective || nextPlan.objective || "",
|
|
1009
|
+
failurePolicy: preferredCompile.failurePolicy
|
|
1010
|
+
|| nextPlan.failurePolicy
|
|
1011
|
+
|| planGraph.failurePolicy
|
|
1012
|
+
|| "continue_independent",
|
|
1013
|
+
nodes: mergedNodes,
|
|
1014
|
+
outputs: { ...(planGraph.outputs || {}) },
|
|
1015
|
+
waitingFor: null,
|
|
1016
|
+
lastStoppedAt: "",
|
|
1017
|
+
lastYieldReason: "",
|
|
1018
|
+
commandLog: planGraph.commandLog || {},
|
|
1019
|
+
owner: nextPlan.owner || planGraph.owner || null,
|
|
1020
|
+
parentGraphId: planGraph.parentGraphId || "",
|
|
1021
|
+
parentNodeId: planGraph.parentNodeId || "",
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
856
1024
|
if (!executionState.graphs || typeof executionState.graphs !== "object") {
|
|
857
1025
|
executionState.graphs = {};
|
|
858
1026
|
}
|
|
@@ -890,45 +1058,80 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
|
|
|
890
1058
|
...advanced.planGraph,
|
|
891
1059
|
specRevision,
|
|
892
1060
|
revision: specRevision,
|
|
893
|
-
commandLog: planGraph.commandLog || {},
|
|
894
|
-
owner: advanced.planGraph.owner || planGraph.owner || null,
|
|
895
|
-
parentGraphId:
|
|
896
|
-
|
|
1061
|
+
commandLog: operation === "create" ? {} : (planGraph.commandLog || {}),
|
|
1062
|
+
owner: advanced.planGraph.owner || (operation === "create" ? nextPlan.owner : planGraph.owner) || null,
|
|
1063
|
+
parentGraphId: operation === "create"
|
|
1064
|
+
? ""
|
|
1065
|
+
: (advanced.planGraph.parentGraphId || planGraph.parentGraphId || ""),
|
|
1066
|
+
parentNodeId: operation === "create"
|
|
1067
|
+
? ""
|
|
1068
|
+
: (advanced.planGraph.parentNodeId || planGraph.parentNodeId || ""),
|
|
897
1069
|
};
|
|
898
1070
|
}
|
|
899
1071
|
if (advanced.advance) advance = advanced.advance;
|
|
900
1072
|
}
|
|
901
1073
|
|
|
902
|
-
const
|
|
903
|
-
const
|
|
1074
|
+
const completedGraph = executionState.planGraph;
|
|
1075
|
+
const terminal = selected.isPrimary
|
|
1076
|
+
? finalizeTerminalPrimaryPlan(executionState, completedGraph, advance)
|
|
1077
|
+
: null;
|
|
1078
|
+
if (!terminal && !selected.isPrimary && advance.yieldReason === "graph_terminal") {
|
|
1079
|
+
// Child TaskLoop graph finished — keep snapshot; parent stays active.
|
|
1080
|
+
const childId = String(completedGraph.graphId || "").trim();
|
|
1081
|
+
if (childId) {
|
|
1082
|
+
executionState.graphs[childId] = {
|
|
1083
|
+
...completedGraph,
|
|
1084
|
+
completedAt: new Date().toISOString(),
|
|
1085
|
+
lastYieldReason: "graph_terminal",
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const live = terminal ? completedGraph : executionState.planGraph;
|
|
1091
|
+
const { readyNodes, waitingNodes } = summarizeReadyWaiting(
|
|
1092
|
+
terminal ? [] : (live.nodes || []),
|
|
1093
|
+
);
|
|
904
1094
|
const payload = accepted({
|
|
905
|
-
graphId: live.graphId,
|
|
906
|
-
commandRevision: live.specRevision,
|
|
907
|
-
stateRevision: live.stateRevision,
|
|
908
|
-
revision: live.specRevision,
|
|
1095
|
+
graphId: terminal ? "" : live.graphId,
|
|
1096
|
+
commandRevision: terminal ? 0 : live.specRevision,
|
|
1097
|
+
stateRevision: terminal ? 0 : live.stateRevision,
|
|
1098
|
+
revision: terminal ? 0 : live.specRevision,
|
|
909
1099
|
changes: {
|
|
910
1100
|
nodesAdded,
|
|
911
1101
|
nodesUpdated: Array.from(new Set(nodesUpdated)),
|
|
1102
|
+
...(replacedGraphId ? { replacedGraphId } : {}),
|
|
1103
|
+
...(terminal ? { archivedGraphId: terminal.archivedGraphId } : {}),
|
|
912
1104
|
},
|
|
913
1105
|
nodesAdded,
|
|
914
1106
|
nodesUpdated: Array.from(new Set(nodesUpdated)),
|
|
915
1107
|
readyNodes,
|
|
916
1108
|
waitingNodes,
|
|
917
|
-
waitingFor: live.waitingFor || null,
|
|
918
|
-
stoppedAt: live.lastStoppedAt || "",
|
|
1109
|
+
waitingFor: terminal ? null : (live.waitingFor || null),
|
|
1110
|
+
stoppedAt: terminal ? "graph_terminal" : (live.lastStoppedAt || ""),
|
|
919
1111
|
advance,
|
|
920
|
-
planView: projectPlanView(live),
|
|
1112
|
+
planView: terminal ? [] : projectPlanView(live),
|
|
921
1113
|
validationWarnings: preferredCompile.warnings || [],
|
|
922
|
-
summary:
|
|
923
|
-
?
|
|
924
|
-
: (advance.
|
|
1114
|
+
summary: terminal
|
|
1115
|
+
? terminal.completionSummary
|
|
1116
|
+
: (advance.waitingFor
|
|
1117
|
+
? `waiting on ${advance.waitingFor.type}:${advance.waitingFor.id || ""}`
|
|
1118
|
+
: (advance.yieldReason || "graph updated")),
|
|
1119
|
+
...(replacedGraphId ? { replacedGraphId } : {}),
|
|
1120
|
+
...(terminal ? {
|
|
1121
|
+
planCompleted: true,
|
|
1122
|
+
archivedGraphId: terminal.archivedGraphId,
|
|
1123
|
+
completionSummary: terminal.completionSummary,
|
|
1124
|
+
} : {}),
|
|
925
1125
|
});
|
|
926
1126
|
|
|
927
|
-
|
|
1127
|
+
if (!terminal) {
|
|
1128
|
+
cacheCommand(executionState.planGraph, commandId, payload);
|
|
1129
|
+
}
|
|
928
1130
|
|
|
929
1131
|
// Agent Loop create enables Plan Mode; TaskLoop child graphs must not.
|
|
1132
|
+
// Skip when create immediately ran to terminal (already exited Plan Mode).
|
|
930
1133
|
let planModeEntered = null;
|
|
931
|
-
if (operation === "create" && payload.status === "accepted") {
|
|
1134
|
+
if (operation === "create" && payload.status === "accepted" && !terminal) {
|
|
932
1135
|
const ownerKind = String(
|
|
933
1136
|
(executionState.planGraph && executionState.planGraph.owner && executionState.planGraph.owner.kind)
|
|
934
1137
|
|| "agent_loop",
|
|
@@ -968,6 +1171,9 @@ function activePlanRequiresExpansion(planGraph = {}) {
|
|
|
968
1171
|
module.exports = {
|
|
969
1172
|
emptyPlanGraphState,
|
|
970
1173
|
ensurePlanGraphState,
|
|
1174
|
+
archivePlanGraph,
|
|
1175
|
+
buildPlanCompletionSummary,
|
|
1176
|
+
finalizeTerminalPrimaryPlan,
|
|
971
1177
|
normalizePlanGraphCommand,
|
|
972
1178
|
normalizeExpandOp,
|
|
973
1179
|
runPlanGraphCommand,
|
|
@@ -54,6 +54,7 @@ function buildImmutablePrefix() {
|
|
|
54
54
|
"- Turning Plan Mode off does not cancel an existing graph or running TaskRuns. Cancel with task_run (standalone) or plan_graph operation=cancel_graph / control.cancel_task (graph-bound).",
|
|
55
55
|
"- When the user enables Plan Mode and no active graph exists, create a plan_graph before performing side effects.",
|
|
56
56
|
"- After an accepted plan_graph create or patch, Runtime automatically advances ready tool nodes. Never invent or request an execute_graph tool.",
|
|
57
|
+
"- When the last plan node finishes (graph_terminal), Runtime archives and clears the active plan, exits auto Plan Mode, and returns a completion summary — narrate that summary to the user. Start a new objective with plan_graph create (create replaces any leftover graph); do not patch a finished plan for a new goal.",
|
|
57
58
|
"- Do not call plan_graph or task_run together with read, read_image, write, edit, bash, or artifact_read in the same assistant turn.",
|
|
58
59
|
"- When an active graph is waiting on a task, advance that node through plan_graph instead of bypassing it with direct workspace tools: use patch.expand_node for execution.kind=expand, control.complete_task (nodeId) for execution.kind=inline_llm, or control.start_task for execution.kind=task_loop.",
|
|
59
60
|
"- TaskLoop start returns childGraphId. While that TaskRun is waiting_model on child root, patch with graphId=<childGraphId> and expand_node nodeId=root (add tool children). Do not ask the user to /plan off.",
|
|
@@ -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;
|
|
@@ -131,7 +139,8 @@ function shouldAutoContinuePlan(executionState = null) {
|
|
|
131
139
|
|
|
132
140
|
/**
|
|
133
141
|
* Internal reminder injected by runtime when the model ends a turn while the
|
|
134
|
-
* plan is still waiting on a task.
|
|
142
|
+
* plan is still waiting on a task. Must NOT reuse the User reminder label —
|
|
143
|
+
* that is reserved for real mid-run user text and pollutes transcript/TUI.
|
|
135
144
|
*/
|
|
136
145
|
function buildPlanAutoContinueReminder(executionState = null) {
|
|
137
146
|
const waiting = executionState
|
|
@@ -140,14 +149,17 @@ function buildPlanAutoContinueReminder(executionState = null) {
|
|
|
140
149
|
? executionState.planGraph.waitingFor
|
|
141
150
|
: null;
|
|
142
151
|
if (!waiting || !waiting.id) return "";
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
{
|
|
150
|
-
|
|
152
|
+
const label = waiting.title || waiting.objective || waiting.reason || waiting.id;
|
|
153
|
+
const lines = [
|
|
154
|
+
"Runtime wake (not a user message): active plan is still waiting.",
|
|
155
|
+
"Continue serving it now via plan_graph "
|
|
156
|
+
+ "(expand_node, control.start_task, or control.complete_task as appropriate). "
|
|
157
|
+
+ "Do not end the turn with text only while this node is waiting.",
|
|
158
|
+
`Prefer serving the current waiting ${waiting.type || "node"}: ${waiting.id}`
|
|
159
|
+
+ (label && label !== waiting.id ? ` — ${label}` : "")
|
|
160
|
+
+ ".",
|
|
161
|
+
];
|
|
162
|
+
return lines.join("\n");
|
|
151
163
|
}
|
|
152
164
|
|
|
153
165
|
module.exports = {
|
|
@@ -156,6 +168,7 @@ module.exports = {
|
|
|
156
168
|
drainUserPrompts,
|
|
157
169
|
clearUserPrompts,
|
|
158
170
|
hasPendingUserPrompts,
|
|
171
|
+
listPendingUserPrompts,
|
|
159
172
|
shouldFrameAsUserReminder,
|
|
160
173
|
formatUserReminderMessage,
|
|
161
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) {
|
|
@@ -4099,6 +4125,8 @@ module.exports = {
|
|
|
4099
4125
|
createInkStreamState,
|
|
4100
4126
|
createThrottledSender,
|
|
4101
4127
|
decorateStaticLogEntry,
|
|
4128
|
+
buildChatLogDisplayLines,
|
|
4129
|
+
expandChatLogPhysicalLines,
|
|
4102
4130
|
bootstrapEnvironment,
|
|
4103
4131
|
buildDirectBusSendRequest,
|
|
4104
4132
|
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.
|
|
@@ -1131,7 +1136,19 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1131
1136
|
|
|
1132
1137
|
// While a native task is in flight, queue an additional user reminder
|
|
1133
1138
|
// for the next LLM turn instead of starting a second NL task.
|
|
1139
|
+
// Slash commands (/model, /plan, …) must still run immediately — same
|
|
1140
|
+
// rule as the REPL path — otherwise they pollute the nudge queue.
|
|
1134
1141
|
if (pendingTaskRef.current) {
|
|
1142
|
+
if (/^\//.test(trimmed)) {
|
|
1143
|
+
runChainRef.current = runChainRef.current
|
|
1144
|
+
.then(() => executeLine(modelText, {
|
|
1145
|
+
modelText,
|
|
1146
|
+
logText,
|
|
1147
|
+
preserveNewlines: attachments.length > 0,
|
|
1148
|
+
}))
|
|
1149
|
+
.catch((err) => appendLogText(`Error: ${err && err.message ? err.message : err}`, "error"));
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1135
1152
|
const { enqueueUserPrompt } = require("../../code/context/userNudge");
|
|
1136
1153
|
const { emptyExecutionState } = require("../../code/context/executionSegment");
|
|
1137
1154
|
if (!props.state || typeof props.state !== "object") {
|
|
@@ -1141,14 +1158,8 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1141
1158
|
if (!props.state.executionState || typeof props.state.executionState !== "object") {
|
|
1142
1159
|
props.state.executionState = emptyExecutionState();
|
|
1143
1160
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
appendLogText(
|
|
1147
|
-
queued.enqueued
|
|
1148
|
-
? `Queued user reminder for next model turn: ${reminderPreview}`
|
|
1149
|
-
: "Could not queue user reminder (empty).",
|
|
1150
|
-
"system",
|
|
1151
|
-
);
|
|
1161
|
+
enqueueUserPrompt(props.state.executionState, modelText);
|
|
1162
|
+
bumpQueue();
|
|
1152
1163
|
return;
|
|
1153
1164
|
}
|
|
1154
1165
|
|
|
@@ -1168,6 +1179,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1168
1179
|
appendLogLine,
|
|
1169
1180
|
flushActiveMerge,
|
|
1170
1181
|
flushTableBuffer,
|
|
1182
|
+
bumpQueue,
|
|
1171
1183
|
props.state,
|
|
1172
1184
|
props.submitUserInteractionAnswer,
|
|
1173
1185
|
refreshPlanUi,
|
|
@@ -1398,6 +1410,25 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1398
1410
|
),
|
|
1399
1411
|
)
|
|
1400
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
|
+
})(),
|
|
1401
1432
|
h(Box, { width: "100%" },
|
|
1402
1433
|
h(MultilineInput, {
|
|
1403
1434
|
value: draft,
|
|
@@ -1457,6 +1488,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1457
1488
|
if (props.state && props.state.executionState) {
|
|
1458
1489
|
clearUserPrompts(props.state.executionState);
|
|
1459
1490
|
}
|
|
1491
|
+
bumpQueue();
|
|
1460
1492
|
} catch { /* ignore */ }
|
|
1461
1493
|
appendLogLine("⚙ Cancellation requested. Stopping the current task...", "system");
|
|
1462
1494
|
setStatus({
|