feat(core,server,web,cli): mid-run steering messages and square-bracket markers (#63)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
abd5b13d52
commit
7584171482
81 files changed
+3139
-930
No files matched your search
@@ -125,7 +125,7 @@ export function registerChatCommand(program: Command, t: Messages): void {
|
||||
// regular input.
|
||||
if (session.resumedHistory) {
|
||||
out.write(`${t.resumedBanner(session.sessionId, session.resumedHistory.length)}\n`);
|
||||
renderHistory(session.resumedHistory, out);
|
||||
renderHistory(session.resumedHistory, out, t);
|
||||
}
|
||||
|
||||
// TTY: raw mode + bracketed paste + PasteFilter; non-TTY (pipe/test): read stdin directly.
|
||||
@@ -148,11 +148,26 @@ export function registerChatCommand(program: Command, t: Messages): void {
|
||||
const rli = rl as unknown as RlInternals;
|
||||
const composer = new LineComposer();
|
||||
|
||||
// Typing lock (owner directive): while the user is composing a line mid-run, streamed
|
||||
// output must not scribble over it — the renderer holds (queues) output while the input
|
||||
// buffer is non-empty and flushes once the line is submitted or cleared. Synced after
|
||||
// readline has processed each chunk (setImmediate: listener order is not guaranteed);
|
||||
// TTY only — non-TTY input (pipe/tests) has no echoed line to protect. The approval
|
||||
// prompt has its own lock (beginUserPrompt), so the hold applies to "running" only.
|
||||
const syncInputHold = (): void => {
|
||||
renderer.setInputHold(state === "running" && rli.line.length > 0);
|
||||
};
|
||||
if (isTTY) {
|
||||
inputStream.on("data", () => setImmediate(syncInputHold));
|
||||
}
|
||||
|
||||
let state: ChatState = "idle";
|
||||
let closed = false;
|
||||
let taskAbort: AbortController | null = null;
|
||||
let pendingLine: ((line: string | null) => void) | null = null;
|
||||
let pendingApproval: ((decision: ApprovalDecision) => void) | null = null;
|
||||
/** A line typed in the running->idle race window (steer refused): becomes the next prompt instead of being dropped. */
|
||||
let queuedPrompt: string | null = null;
|
||||
|
||||
const cleanup = () => {
|
||||
if (!isTTY) return;
|
||||
@@ -221,8 +236,24 @@ export function registerChatCommand(program: Command, t: Messages): void {
|
||||
pendingApproval = null;
|
||||
// Tool approval defaults to allow: pressing Enter (empty input) is treated as allow.
|
||||
resolve(parseApprovalAnswer(line, "allow"));
|
||||
return;
|
||||
}
|
||||
// running: a non-empty line becomes a steering message for the running Task — core
|
||||
// delivers it between turns as a standalone [user_steering] user message, so the
|
||||
// model sees it without the loop being interrupted. Empty lines are still ignored.
|
||||
// steer() returning false (race: the task just finished) must not drop the line:
|
||||
// it is stashed and becomes the next normal prompt as soon as the loop asks again.
|
||||
if (state === "running") {
|
||||
const text = line.trim();
|
||||
if (text.length === 0) return;
|
||||
if (session.steer(text)) {
|
||||
// Printed via the renderer while the typing hold is still engaged, so the ack
|
||||
// lands before the held stream output flushes underneath it.
|
||||
renderer.printLine(dim(t.steerQueued(text)));
|
||||
} else {
|
||||
queuedPrompt = text;
|
||||
}
|
||||
}
|
||||
// running: ignore any line typed at this moment.
|
||||
});
|
||||
|
||||
rl.on("SIGINT", () => {
|
||||
@@ -272,6 +303,15 @@ export function registerChatCommand(program: Command, t: Messages): void {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
// A line typed in the running->idle race window (steer refused): submit it as this
|
||||
// prompt immediately — the text is already echoed on screen, no re-prompt needed.
|
||||
if (queuedPrompt !== null) {
|
||||
const line = queuedPrompt;
|
||||
queuedPrompt = null;
|
||||
state = "idle";
|
||||
resolve(line);
|
||||
return;
|
||||
}
|
||||
state = "idle";
|
||||
pendingLine = resolve;
|
||||
composer.reset();
|
||||
|
||||
Reference in new issue
Block a user