nolo-cli 0.37.0-alpha.3 → 0.37.0-alpha.4

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/index.js CHANGED
@@ -824,7 +824,7 @@ async function runScript(script, forwardedArgs, env) {
824
824
  return proc.exited;
825
825
  }
826
826
  async function launchTuiWorkspace(args2) {
827
- const { startTuiWorkspace } = await import("./readlineWorkspace-WOKEACUU.js");
827
+ const { startTuiWorkspace } = await import("./readlineWorkspace-PLJJPDP2.js");
828
828
  const { createTuiSummaryLlmCaller } = await import("./tuiSummaryLlmCaller-SHRB4RWM.js");
829
829
  return startTuiWorkspace({
830
830
  ...args2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nolo-cli",
3
- "version": "0.37.0-alpha.3",
3
+ "version": "0.37.0-alpha.4",
4
4
  "type": "module",
5
5
  "description": "Agent-first terminal workspace for Nolo",
6
6
  "bin": {
@@ -7094,6 +7094,219 @@ function waitForRawActionGate(input, output, gate, spawnRunner, hooks) {
7094
7094
  }
7095
7095
  });
7096
7096
  }
7097
+ async function runOneAgentTurn(ctx, inputMsg, imageUrls, actionGateHandler, confirmDestructiveAction) {
7098
+ const scheduleTitlePatchSync = (runResult) => {
7099
+ if (!runResult.titlePatchPromise || !runResult.dialogId) return;
7100
+ const patchDialogId = runResult.dialogId;
7101
+ runResult.titlePatchPromise.then((patchedTitle) => {
7102
+ if (!patchedTitle || ctx.sessionEnded) return;
7103
+ if (ctx.state.dialogId !== patchDialogId) return;
7104
+ if (ctx.state.dialogLabel === patchedTitle) return;
7105
+ ctx.state = {
7106
+ ...ctx.state,
7107
+ dialogLabel: patchedTitle,
7108
+ dialogTitle: patchedTitle
7109
+ };
7110
+ ctx.syncWindowTitle();
7111
+ }).catch(() => {
7112
+ });
7113
+ };
7114
+ let req = createTurnRequest(inputMsg);
7115
+ if (req.event.kind === "child-run-completed") {
7116
+ const nowMs = Date.now();
7117
+ const { remainingRuns, claimedRunIds } = filterUnclaimedChildRuns(
7118
+ req.event.runs,
7119
+ { env: ctx.effectiveEnv, now: () => nowMs }
7120
+ );
7121
+ for (const runId of claimedRunIds) {
7122
+ ctx.runCompletionWatcher.markAcknowledged(runId);
7123
+ }
7124
+ if (remainingRuns.length === 0) {
7125
+ for (const r of req.event.runs) {
7126
+ ctx.runCompletionWatcher.markAcknowledged(r.runId);
7127
+ }
7128
+ return { ok: true, aborted: false };
7129
+ }
7130
+ for (const r of remainingRuns) {
7131
+ ctx.runCompletionWatcher.markAcknowledged(r.runId);
7132
+ }
7133
+ if (remainingRuns.length !== req.event.runs.length) {
7134
+ const text = buildWakeMessage(remainingRuns, nowMs);
7135
+ const displayText = buildWakeDisplayText(remainingRuns, nowMs);
7136
+ const updatedEvent = {
7137
+ kind: "child-run-completed",
7138
+ runs: remainingRuns,
7139
+ text,
7140
+ displayText
7141
+ };
7142
+ req = {
7143
+ text,
7144
+ event: updatedEvent
7145
+ };
7146
+ }
7147
+ }
7148
+ const message = req.text;
7149
+ ctx.forcedStop = false;
7150
+ ctx.turnEpoch += 1;
7151
+ const myEpoch = ctx.turnEpoch;
7152
+ ctx.history.followBottom = true;
7153
+ const isInternalEvent = req.event.kind !== "user";
7154
+ const transcriptText = req.event.kind === "child-run-completed" ? req.event.displayText ?? req.event.text : message;
7155
+ startTurn(ctx.history, isInternalEvent ? "assistant" : "user");
7156
+ appendToCurrentTurn(
7157
+ ctx.history,
7158
+ isInternalEvent ? dimCliText(transcriptText, resolveCliColorEnabled()) : transcriptText
7159
+ );
7160
+ finalizeCurrentTurn(ctx.history);
7161
+ ctx.renderHistoryToOutput();
7162
+ if (ctx.fixedInput.active) ctx.fixedInput.repaint(ctx.buffer, ctx.cursorPos);
7163
+ startTurn(ctx.history, "assistant");
7164
+ const agentOutput = isInteractiveInput(ctx.input) ? createHistoryOutputStream(ctx.history, () => {
7165
+ ctx.scheduleRender();
7166
+ }) : ctx.output;
7167
+ const requestUserChoice = isInteractiveInput(ctx.input) && ctx.dialogHost ? async (choiceReq) => {
7168
+ ctx.modalOwnsKeyboard = true;
7169
+ try {
7170
+ return await ctx.dialogHost.run(
7171
+ (anchor) => runAskChoiceDialog({
7172
+ request: choiceReq,
7173
+ input: ctx.input,
7174
+ output: ctx.output,
7175
+ ...anchor
7176
+ })
7177
+ );
7178
+ } catch {
7179
+ return { kind: "cancelled" };
7180
+ } finally {
7181
+ ctx.composerDecoderDrain?.();
7182
+ ctx.modalOwnsKeyboard = false;
7183
+ }
7184
+ } : void 0;
7185
+ try {
7186
+ ctx.activeTurnAbort = new AbortController();
7187
+ ctx.activeTurnEpoch = myEpoch;
7188
+ const runResult = await runAgentChat(
7189
+ ctx.options.scriptDir,
7190
+ ctx.state,
7191
+ message,
7192
+ ctx.options.env ?? process.env,
7193
+ agentOutput,
7194
+ ctx.options.agentRunner,
7195
+ {
7196
+ ...imageUrls.length > 0 ? { imageUrls } : {},
7197
+ actionGateHandler,
7198
+ ...confirmDestructiveAction ? { confirmDestructiveAction } : {},
7199
+ ...requestUserChoice ? { requestUserChoice } : {},
7200
+ abortSignal: ctx.activeTurnAbort.signal,
7201
+ pastedTextStore: ctx.pasteStore,
7202
+ activityReporter: ctx.activityReporter,
7203
+ onAgentRunStatus: (snapshot) => {
7204
+ if (snapshot) {
7205
+ ctx.activityIndicator.updateAgentRun(snapshot);
7206
+ ctx.runRegistryPoller.ensureRunning();
7207
+ } else {
7208
+ ctx.activityIndicator.clearAgentRun();
7209
+ }
7210
+ }
7211
+ }
7212
+ );
7213
+ const wasForceStopped = ctx.forcedStopEpoch === myEpoch;
7214
+ if (wasForceStopped) {
7215
+ if (runResult.dialogId || runResult.turnTokens) {
7216
+ const nextDialogKey = runResult.dialogId ? runResult.dialogId === ctx.state.dialogId && ctx.state.dialogKey ? ctx.state.dialogKey : ctx.state.dialogOwnerId ? `dialog-${ctx.state.dialogOwnerId}-${runResult.dialogId}` : void 0 : ctx.state.dialogKey;
7217
+ ctx.state = {
7218
+ ...ctx.state,
7219
+ ...runResult.dialogId ? {
7220
+ dialogId: runResult.dialogId,
7221
+ dialogKey: nextDialogKey,
7222
+ dialogLabel: runResult.title || runResult.dialogId,
7223
+ ...runResult.title ? { dialogTitle: runResult.title } : {}
7224
+ } : {},
7225
+ ...runResult.turnTokens ? { turnTokens: runResult.turnTokens } : {},
7226
+ ...runResult.cachedMemoryOverlay !== void 0 ? { cachedMemoryOverlay: runResult.cachedMemoryOverlay } : {}
7227
+ };
7228
+ if (runResult.dialogId && nextDialogKey) {
7229
+ ctx.refreshDialogTotalCredits(runResult.dialogId, nextDialogKey);
7230
+ }
7231
+ }
7232
+ scheduleTitlePatchSync(runResult);
7233
+ return { ok: false, aborted: true };
7234
+ }
7235
+ const wasAborted = ctx.activeTurnAbort.signal.aborted;
7236
+ ctx.activeTurnAbort = null;
7237
+ if (shouldEmitTerminalBell({
7238
+ wasAborted,
7239
+ streamInterrupted: runResult.streamInterrupted,
7240
+ exitCode: runResult.exitCode,
7241
+ interactive: isInteractiveInput(ctx.input)
7242
+ })) {
7243
+ emitTerminalBell(ctx.output);
7244
+ }
7245
+ if (isInteractiveInput(ctx.input)) {
7246
+ finalizeCurrentTurn(ctx.history);
7247
+ ctx.flushPendingRender();
7248
+ ctx.renderHistoryToOutput();
7249
+ if (ctx.fixedInput.active) ctx.fixedInput.repaint(ctx.buffer, ctx.cursorPos);
7250
+ }
7251
+ if (wasAborted) {
7252
+ if (runResult.pendingToolName) {
7253
+ ctx.emitCommandOutput(t("turnStoppedToolPending", runResult.pendingToolName));
7254
+ } else {
7255
+ ctx.emitCommandOutput(t("turnStopped"));
7256
+ }
7257
+ }
7258
+ if (runResult.dialogId || runResult.turnTokens || runResult.contextWindow) {
7259
+ const nextDialogKey = runResult.dialogId ? runResult.dialogId === ctx.state.dialogId && ctx.state.dialogKey ? ctx.state.dialogKey : ctx.state.dialogOwnerId ? `dialog-${ctx.state.dialogOwnerId}-${runResult.dialogId}` : void 0 : ctx.state.dialogKey;
7260
+ ctx.state = {
7261
+ ...ctx.state,
7262
+ ...runResult.dialogId ? {
7263
+ dialogId: runResult.dialogId,
7264
+ dialogKey: nextDialogKey,
7265
+ dialogLabel: runResult.title || runResult.dialogId,
7266
+ ...runResult.title ? { dialogTitle: runResult.title } : {}
7267
+ } : {},
7268
+ ...runResult.turnTokens ? { turnTokens: runResult.turnTokens } : {},
7269
+ ...runResult.contextWindow ? { contextWindow: runResult.contextWindow } : {},
7270
+ // input_tokens 是累计上下文输入(含历史消息),把它持久化到
7271
+ // estimatedContextTokens:下一轮若 provider 不返回 usage,context
7272
+ // chip 仍显示真实累计占用而不是回退到启动时的静态估算。
7273
+ ...runResult.turnTokens && runResult.turnTokens.input > 0 ? { estimatedContextTokens: runResult.turnTokens.input } : {},
7274
+ ...runResult.cachedMemoryOverlay !== void 0 ? { cachedMemoryOverlay: runResult.cachedMemoryOverlay } : {}
7275
+ };
7276
+ if (runResult.dialogId && nextDialogKey) {
7277
+ ctx.refreshDialogTotalCredits(runResult.dialogId, nextDialogKey);
7278
+ }
7279
+ }
7280
+ scheduleTitlePatchSync(runResult);
7281
+ if (!wasAborted && runResult.exitCode !== 0) {
7282
+ if (isBalanceExhaustedError(runResult.localError) && runResult.dialogId) {
7283
+ ctx.emitCommandOutput(t("balanceExhaustedHint"));
7284
+ } else if (isQuotaExhaustedError(runResult.localError)) {
7285
+ ctx.emitCommandOutput(t("quotaExhaustedHint"));
7286
+ } else if (runResult.dialogId) {
7287
+ ctx.emitCommandOutput(t("dialogPreservedHint"));
7288
+ } else {
7289
+ ctx.emitCommandOutput(t("dialogNotSavedHint"));
7290
+ }
7291
+ }
7292
+ return { ok: !wasAborted, aborted: wasAborted };
7293
+ } finally {
7294
+ ctx.activityIndicator.stop();
7295
+ ctx.activeTurnAbort = null;
7296
+ }
7297
+ }
7298
+ function ensureChatQueueBinding(ctx, actionGateHandler, confirmDestructiveAction) {
7299
+ if (ctx.chatQueueBinding) return ctx.chatQueueBinding;
7300
+ ctx.chatQueueBinding = createChatQueueTuiBinding(async (text) => {
7301
+ return runOneAgentTurn(ctx, text, [], actionGateHandler, confirmDestructiveAction);
7302
+ });
7303
+ return ctx.chatQueueBinding;
7304
+ }
7305
+ function preemptAndAbortForDrain(binding, activeTurnAbort) {
7306
+ if (binding.preemptForDrain() && activeTurnAbort) {
7307
+ activeTurnAbort.abort();
7308
+ }
7309
+ }
7097
7310
  var latestWorkspaceThemeOwner = 0;
7098
7311
  async function runTuiWorkspace(options) {
7099
7312
  initCliLocale(options.env ?? process.env);
@@ -7373,220 +7586,6 @@ ${content}`);
7373
7586
  });
7374
7587
  };
7375
7588
  let composerDecoderDrain = null;
7376
- const runOneAgentTurn = async (inputMsg, imageUrls, actionGateHandler, confirmDestructiveAction) => {
7377
- const scheduleTitlePatchSync = (runResult) => {
7378
- if (!runResult.titlePatchPromise || !runResult.dialogId) return;
7379
- const patchDialogId = runResult.dialogId;
7380
- runResult.titlePatchPromise.then((patchedTitle) => {
7381
- if (!patchedTitle || sessionEnded) return;
7382
- if (state.dialogId !== patchDialogId) return;
7383
- if (state.dialogLabel === patchedTitle) return;
7384
- state = {
7385
- ...state,
7386
- dialogLabel: patchedTitle,
7387
- dialogTitle: patchedTitle
7388
- };
7389
- syncWindowTitle();
7390
- }).catch(() => {
7391
- });
7392
- };
7393
- let req = createTurnRequest(inputMsg);
7394
- if (req.event.kind === "child-run-completed") {
7395
- const nowMs = Date.now();
7396
- const { remainingRuns, claimedRunIds } = filterUnclaimedChildRuns(
7397
- req.event.runs,
7398
- { env: effectiveEnv, now: () => nowMs }
7399
- );
7400
- for (const runId of claimedRunIds) {
7401
- runCompletionWatcher.markAcknowledged(runId);
7402
- }
7403
- if (remainingRuns.length === 0) {
7404
- for (const r of req.event.runs) {
7405
- runCompletionWatcher.markAcknowledged(r.runId);
7406
- }
7407
- return { ok: true, aborted: false };
7408
- }
7409
- for (const r of remainingRuns) {
7410
- runCompletionWatcher.markAcknowledged(r.runId);
7411
- }
7412
- if (remainingRuns.length !== req.event.runs.length) {
7413
- const text = buildWakeMessage(remainingRuns, nowMs);
7414
- const displayText = buildWakeDisplayText(remainingRuns, nowMs);
7415
- const updatedEvent = {
7416
- kind: "child-run-completed",
7417
- runs: remainingRuns,
7418
- text,
7419
- displayText
7420
- };
7421
- req = {
7422
- text,
7423
- event: updatedEvent
7424
- };
7425
- }
7426
- }
7427
- const message = req.text;
7428
- forcedStop = false;
7429
- turnEpoch += 1;
7430
- const myEpoch = turnEpoch;
7431
- history.followBottom = true;
7432
- const isInternalEvent = req.event.kind !== "user";
7433
- const transcriptText = req.event.kind === "child-run-completed" ? req.event.displayText ?? req.event.text : message;
7434
- startTurn(history, isInternalEvent ? "assistant" : "user");
7435
- appendToCurrentTurn(
7436
- history,
7437
- isInternalEvent ? dimCliText(transcriptText, resolveCliColorEnabled()) : transcriptText
7438
- );
7439
- finalizeCurrentTurn(history);
7440
- renderHistoryToOutput();
7441
- if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
7442
- startTurn(history, "assistant");
7443
- const agentOutput = isInteractiveInput(input) ? createHistoryOutputStream(history, () => {
7444
- scheduleRender();
7445
- }) : output;
7446
- const requestUserChoice = isInteractiveInput(input) && dialogHost ? async (req2) => {
7447
- modalOwnsKeyboard = true;
7448
- try {
7449
- return await dialogHost.run(
7450
- (anchor) => runAskChoiceDialog({
7451
- request: req2,
7452
- input,
7453
- output,
7454
- ...anchor
7455
- })
7456
- );
7457
- } catch {
7458
- return { kind: "cancelled" };
7459
- } finally {
7460
- composerDecoderDrain?.();
7461
- modalOwnsKeyboard = false;
7462
- }
7463
- } : void 0;
7464
- try {
7465
- activeTurnAbort = new AbortController();
7466
- activeTurnEpoch = myEpoch;
7467
- const runResult = await runAgentChat(
7468
- options.scriptDir,
7469
- state,
7470
- message,
7471
- options.env ?? process.env,
7472
- agentOutput,
7473
- options.agentRunner,
7474
- {
7475
- ...imageUrls.length > 0 ? { imageUrls } : {},
7476
- actionGateHandler,
7477
- ...confirmDestructiveAction ? { confirmDestructiveAction } : {},
7478
- ...requestUserChoice ? { requestUserChoice } : {},
7479
- abortSignal: activeTurnAbort.signal,
7480
- pastedTextStore: pasteStore,
7481
- activityReporter,
7482
- onAgentRunStatus: (snapshot) => {
7483
- if (snapshot) {
7484
- activityIndicator.updateAgentRun(snapshot);
7485
- runRegistryPoller.ensureRunning();
7486
- } else {
7487
- activityIndicator.clearAgentRun();
7488
- }
7489
- }
7490
- }
7491
- );
7492
- const wasForceStopped = forcedStopEpoch === myEpoch;
7493
- if (wasForceStopped) {
7494
- if (runResult.dialogId || runResult.turnTokens) {
7495
- const nextDialogKey = runResult.dialogId ? runResult.dialogId === state.dialogId && state.dialogKey ? state.dialogKey : state.dialogOwnerId ? `dialog-${state.dialogOwnerId}-${runResult.dialogId}` : void 0 : state.dialogKey;
7496
- state = {
7497
- ...state,
7498
- ...runResult.dialogId ? {
7499
- dialogId: runResult.dialogId,
7500
- dialogKey: nextDialogKey,
7501
- dialogLabel: runResult.title || runResult.dialogId,
7502
- ...runResult.title ? { dialogTitle: runResult.title } : {}
7503
- } : {},
7504
- ...runResult.turnTokens ? { turnTokens: runResult.turnTokens } : {},
7505
- ...runResult.cachedMemoryOverlay !== void 0 ? { cachedMemoryOverlay: runResult.cachedMemoryOverlay } : {}
7506
- };
7507
- if (runResult.dialogId && nextDialogKey) {
7508
- refreshDialogTotalCredits(runResult.dialogId, nextDialogKey);
7509
- }
7510
- }
7511
- scheduleTitlePatchSync(runResult);
7512
- return { ok: false, aborted: true };
7513
- }
7514
- const wasAborted = activeTurnAbort.signal.aborted;
7515
- activeTurnAbort = null;
7516
- if (shouldEmitTerminalBell({
7517
- wasAborted,
7518
- streamInterrupted: runResult.streamInterrupted,
7519
- exitCode: runResult.exitCode,
7520
- interactive: isInteractiveInput(input)
7521
- })) {
7522
- emitTerminalBell(output);
7523
- }
7524
- if (isInteractiveInput(input)) {
7525
- finalizeCurrentTurn(history);
7526
- flushPendingRender();
7527
- renderHistoryToOutput();
7528
- if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
7529
- }
7530
- if (wasAborted) {
7531
- if (runResult.pendingToolName) {
7532
- emitCommandOutput(t("turnStoppedToolPending", runResult.pendingToolName));
7533
- } else {
7534
- emitCommandOutput(t("turnStopped"));
7535
- }
7536
- }
7537
- if (runResult.dialogId || runResult.turnTokens || runResult.contextWindow) {
7538
- const nextDialogKey = runResult.dialogId ? runResult.dialogId === state.dialogId && state.dialogKey ? state.dialogKey : state.dialogOwnerId ? `dialog-${state.dialogOwnerId}-${runResult.dialogId}` : void 0 : state.dialogKey;
7539
- state = {
7540
- ...state,
7541
- ...runResult.dialogId ? {
7542
- dialogId: runResult.dialogId,
7543
- dialogKey: nextDialogKey,
7544
- dialogLabel: runResult.title || runResult.dialogId,
7545
- ...runResult.title ? { dialogTitle: runResult.title } : {}
7546
- } : {},
7547
- ...runResult.turnTokens ? { turnTokens: runResult.turnTokens } : {},
7548
- ...runResult.contextWindow ? { contextWindow: runResult.contextWindow } : {},
7549
- // input_tokens 是累计上下文输入(含历史消息),把它持久化到
7550
- // estimatedContextTokens:下一轮若 provider 不返回 usage,context
7551
- // chip 仍显示真实累计占用而不是回退到启动时的静态估算。
7552
- ...runResult.turnTokens && runResult.turnTokens.input > 0 ? { estimatedContextTokens: runResult.turnTokens.input } : {},
7553
- ...runResult.cachedMemoryOverlay !== void 0 ? { cachedMemoryOverlay: runResult.cachedMemoryOverlay } : {}
7554
- };
7555
- if (runResult.dialogId && nextDialogKey) {
7556
- refreshDialogTotalCredits(runResult.dialogId, nextDialogKey);
7557
- }
7558
- }
7559
- scheduleTitlePatchSync(runResult);
7560
- if (!wasAborted && runResult.exitCode !== 0) {
7561
- if (isBalanceExhaustedError(runResult.localError) && runResult.dialogId) {
7562
- emitCommandOutput(t("balanceExhaustedHint"));
7563
- } else if (isQuotaExhaustedError(runResult.localError)) {
7564
- emitCommandOutput(t("quotaExhaustedHint"));
7565
- } else if (runResult.dialogId) {
7566
- emitCommandOutput(t("dialogPreservedHint"));
7567
- } else {
7568
- emitCommandOutput(t("dialogNotSavedHint"));
7569
- }
7570
- }
7571
- return { ok: !wasAborted, aborted: wasAborted };
7572
- } finally {
7573
- activityIndicator.stop();
7574
- activeTurnAbort = null;
7575
- }
7576
- };
7577
- let chatQueueBinding = null;
7578
- const ensureChatQueueBinding = (actionGateHandler, confirmDestructiveAction) => {
7579
- if (chatQueueBinding) return chatQueueBinding;
7580
- chatQueueBinding = createChatQueueTuiBinding(async (text) => {
7581
- return runOneAgentTurn(text, [], actionGateHandler, confirmDestructiveAction);
7582
- });
7583
- return chatQueueBinding;
7584
- };
7585
- const preemptAndAbortForDrain = (binding) => {
7586
- if (binding.preemptForDrain() && activeTurnAbort) {
7587
- activeTurnAbort.abort();
7588
- }
7589
- };
7590
7589
  const emitCommandOutput = (text, command = "") => {
7591
7590
  if (!text) return;
7592
7591
  if (!isInteractiveInput(input)) {
@@ -7599,6 +7598,92 @@ ${content}`);
7599
7598
  renderHistoryToOutput();
7600
7599
  if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
7601
7600
  };
7601
+ let chatQueueBinding = null;
7602
+ const turnCtx = {
7603
+ get state() {
7604
+ return state;
7605
+ },
7606
+ set state(v) {
7607
+ state = v;
7608
+ },
7609
+ get forcedStop() {
7610
+ return forcedStop;
7611
+ },
7612
+ set forcedStop(v) {
7613
+ forcedStop = v;
7614
+ },
7615
+ get forcedStopEpoch() {
7616
+ return forcedStopEpoch;
7617
+ },
7618
+ set forcedStopEpoch(v) {
7619
+ forcedStopEpoch = v;
7620
+ },
7621
+ get turnEpoch() {
7622
+ return turnEpoch;
7623
+ },
7624
+ set turnEpoch(v) {
7625
+ turnEpoch = v;
7626
+ },
7627
+ get activeTurnAbort() {
7628
+ return activeTurnAbort;
7629
+ },
7630
+ set activeTurnAbort(v) {
7631
+ activeTurnAbort = v;
7632
+ },
7633
+ get activeTurnEpoch() {
7634
+ return activeTurnEpoch;
7635
+ },
7636
+ set activeTurnEpoch(v) {
7637
+ activeTurnEpoch = v;
7638
+ },
7639
+ get modalOwnsKeyboard() {
7640
+ return modalOwnsKeyboard;
7641
+ },
7642
+ set modalOwnsKeyboard(v) {
7643
+ modalOwnsKeyboard = v;
7644
+ },
7645
+ get composerDecoderDrain() {
7646
+ return composerDecoderDrain;
7647
+ },
7648
+ set composerDecoderDrain(v) {
7649
+ composerDecoderDrain = v;
7650
+ },
7651
+ get chatQueueBinding() {
7652
+ return chatQueueBinding;
7653
+ },
7654
+ set chatQueueBinding(v) {
7655
+ chatQueueBinding = v;
7656
+ },
7657
+ get sessionEnded() {
7658
+ return sessionEnded;
7659
+ },
7660
+ get buffer() {
7661
+ return buffer;
7662
+ },
7663
+ get cursorPos() {
7664
+ return cursorPos;
7665
+ },
7666
+ get fixedInput() {
7667
+ return fixedInput;
7668
+ },
7669
+ options,
7670
+ effectiveEnv,
7671
+ history,
7672
+ activityIndicator,
7673
+ activityReporter,
7674
+ runRegistryPoller,
7675
+ runCompletionWatcher,
7676
+ pasteStore,
7677
+ dialogHost,
7678
+ input,
7679
+ output,
7680
+ syncWindowTitle,
7681
+ renderHistoryToOutput,
7682
+ scheduleRender,
7683
+ flushPendingRender,
7684
+ refreshDialogTotalCredits,
7685
+ emitCommandOutput
7686
+ };
7602
7687
  const persistExplicitAgentSwitch = (previousAgentKey) => {
7603
7688
  if (state.agentKey === previousAgentKey) return false;
7604
7689
  persistAgentSelection(
@@ -8020,10 +8105,11 @@ ${stderrText.trim()}
8020
8105
  attachedImages: []
8021
8106
  };
8022
8107
  history.followBottom = true;
8023
- const binding = ensureChatQueueBinding(actionGateHandler, confirmDestructiveAction);
8108
+ const binding = ensureChatQueueBinding(turnCtx, actionGateHandler, confirmDestructiveAction);
8024
8109
  binding.notifyTurnStart();
8025
8110
  try {
8026
8111
  const outcome = await runOneAgentTurn(
8112
+ turnCtx,
8027
8113
  result.action.message,
8028
8114
  imageUrls,
8029
8115
  actionGateHandler,
@@ -8182,12 +8268,13 @@ ${err.message}` : ""}`
8182
8268
  const runIdleTextTurn = async (inputMsg) => {
8183
8269
  const req = createTurnRequest(inputMsg);
8184
8270
  const { actionGateHandler, confirmDestructiveAction } = buildInteractiveTurnHandlers();
8185
- const binding = ensureChatQueueBinding(actionGateHandler, confirmDestructiveAction);
8271
+ const binding = ensureChatQueueBinding(turnCtx, actionGateHandler, confirmDestructiveAction);
8186
8272
  binding.notifyTurnStart();
8187
8273
  busy = true;
8188
8274
  fixedInput.enterOutputMode(req.text);
8189
8275
  try {
8190
8276
  const outcome = await runOneAgentTurn(
8277
+ turnCtx,
8191
8278
  req,
8192
8279
  [],
8193
8280
  actionGateHandler,
@@ -8211,7 +8298,7 @@ ${err.message}` : ""}`
8211
8298
  if (done) return;
8212
8299
  if (busy || fixedInput.isPaused()) {
8213
8300
  const { actionGateHandler, confirmDestructiveAction } = buildInteractiveTurnHandlers();
8214
- ensureChatQueueBinding(actionGateHandler, confirmDestructiveAction).enqueue(event);
8301
+ ensureChatQueueBinding(turnCtx, actionGateHandler, confirmDestructiveAction).enqueue(event);
8215
8302
  if (fixedInput.active && !fixedInput.isPaused()) {
8216
8303
  fixedInput.repaint(buffer, cursorPos);
8217
8304
  }
@@ -8271,6 +8358,109 @@ ${err.message}` : ""}`
8271
8358
  paintFrame(buffer);
8272
8359
  }, 60);
8273
8360
  };
8361
+ const handleCtrlCKey = async (busyLock) => {
8362
+ if (busyLock && activeTurnAbort) {
8363
+ const stopBinding = chatQueueBinding;
8364
+ if (stopBinding && stopBinding.queueLength() > 0) {
8365
+ stopBinding.preemptForStop();
8366
+ }
8367
+ activityIndicator.markStopping();
8368
+ activeTurnAbort.abort();
8369
+ return;
8370
+ }
8371
+ const now = Date.now();
8372
+ const hasSelection = selectionState.anchor !== null && selectionState.head !== null && !areSelectionPointsEqual(selectionState.anchor, selectionState.head);
8373
+ if (hasSelection) {
8374
+ const tty = output;
8375
+ const columns = tty.columns ?? 80;
8376
+ const contentWidth = Math.max(1, columns - 1);
8377
+ const textToCopy = extractSelectedText(
8378
+ history,
8379
+ selectionState.anchor,
8380
+ selectionState.head,
8381
+ contentWidth
8382
+ );
8383
+ clearSelection();
8384
+ if (textToCopy.length > 0) {
8385
+ try {
8386
+ await writeClipboard2(textToCopy);
8387
+ emitCommandOutput(t("copiedSelection"));
8388
+ } catch (error) {
8389
+ emitCommandOutput(
8390
+ `[nolo] ${t("copyFailed")}: ${toErrorMessage(error)}`
8391
+ );
8392
+ }
8393
+ }
8394
+ paintFrame(buffer);
8395
+ return;
8396
+ }
8397
+ if (selectionState.anchor) {
8398
+ clearSelection();
8399
+ }
8400
+ if (buffer.length > 0) {
8401
+ buffer = "";
8402
+ cursorPos = 0;
8403
+ if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
8404
+ emitCommandOutput(t("ctrlCClearedDraft"));
8405
+ return;
8406
+ }
8407
+ if (lastCtrlCDoublePress !== null && now - lastCtrlCDoublePress <= 1e3) {
8408
+ lastCtrlCDoublePress = null;
8409
+ fixedInput.disable();
8410
+ finish();
8411
+ return;
8412
+ }
8413
+ lastCtrlCDoublePress = now;
8414
+ emitCommandOutput(t("ctrlCExitHint"));
8415
+ };
8416
+ const handleBusyLocalSlash = async (submittedText, busySlashCommand) => {
8417
+ releaseCollapsedPasteReferences(submittedText, pasteStore);
8418
+ buffer = "";
8419
+ cursorPos = 0;
8420
+ if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
8421
+ const beforeAgentKey = state.agentKey;
8422
+ const res = handleTuiInput(submittedText, state);
8423
+ if (res.action?.type === "theme-refresh") {
8424
+ state = res.nextState;
8425
+ const detected = await detectTerminalBackground({
8426
+ stdin: input,
8427
+ stdout: output,
8428
+ allowSystemFallback: true
8429
+ });
8430
+ let refreshMsg = "";
8431
+ if (detected && applyDetectedBackground(detected)) {
8432
+ refreshMsg = t("themeRefreshed", detected.brightness);
8433
+ } else if (detected) {
8434
+ refreshMsg = t("themeRefreshed", detected.brightness);
8435
+ } else {
8436
+ refreshMsg = t("themeRefreshFailed");
8437
+ }
8438
+ if (refreshMsg) {
8439
+ output.write(`${refreshMsg}
8440
+ `);
8441
+ }
8442
+ } else if (res.action) {
8443
+ output.write(
8444
+ "Model picker isn't available while a reply is running. Use `/switch <name>` to switch now (takes effect on the next turn), or wait for the reply to finish.\n"
8445
+ );
8446
+ } else {
8447
+ state = res.nextState;
8448
+ let msg = res.output;
8449
+ if (busySlashCommand === "/switch" && persistExplicitAgentSwitch(beforeAgentKey)) {
8450
+ const hint = "Note: the new model takes effect on the next turn. Switching models may consume more tokens because the conversation context is re-sent to the new model.";
8451
+ msg = msg ? `${msg}
8452
+ ${hint}` : hint;
8453
+ }
8454
+ if (msg) {
8455
+ output.write(`${msg}
8456
+ `);
8457
+ }
8458
+ }
8459
+ if (busySlashCommand === "/theme") {
8460
+ renderHistoryToOutput();
8461
+ if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
8462
+ }
8463
+ };
8274
8464
  const handleInputToken = async (sequence) => {
8275
8465
  if (done) return;
8276
8466
  if (modalOwnsKeyboard) {
@@ -8293,7 +8483,7 @@ ${merged}` : merged;
8293
8483
  t(busy ? "flushQueuedBusyHint" : "flushQueuedIdleHint", String(totalCount))
8294
8484
  );
8295
8485
  if (busy) {
8296
- preemptAndAbortForDrain(chatQueueBinding);
8486
+ preemptAndAbortForDrain(chatQueueBinding, activeTurnAbort);
8297
8487
  return;
8298
8488
  }
8299
8489
  buffer = "";
@@ -8400,59 +8590,7 @@ ${merged}` : merged;
8400
8590
  return;
8401
8591
  }
8402
8592
  if (sequence === "") {
8403
- if (busyLock && activeTurnAbort) {
8404
- const stopBinding = chatQueueBinding;
8405
- if (stopBinding && stopBinding.queueLength() > 0) {
8406
- stopBinding.preemptForStop();
8407
- }
8408
- activityIndicator.markStopping();
8409
- activeTurnAbort.abort();
8410
- return;
8411
- }
8412
- const now = Date.now();
8413
- const hasSelection = selectionState.anchor !== null && selectionState.head !== null && !areSelectionPointsEqual(selectionState.anchor, selectionState.head);
8414
- if (hasSelection) {
8415
- const tty = output;
8416
- const columns = tty.columns ?? 80;
8417
- const contentWidth = Math.max(1, columns - 1);
8418
- const textToCopy = extractSelectedText(
8419
- history,
8420
- selectionState.anchor,
8421
- selectionState.head,
8422
- contentWidth
8423
- );
8424
- clearSelection();
8425
- if (textToCopy.length > 0) {
8426
- try {
8427
- await writeClipboard2(textToCopy);
8428
- emitCommandOutput(t("copiedSelection"));
8429
- } catch (error) {
8430
- emitCommandOutput(
8431
- `[nolo] ${t("copyFailed")}: ${toErrorMessage(error)}`
8432
- );
8433
- }
8434
- }
8435
- paintFrame(buffer);
8436
- return;
8437
- }
8438
- if (selectionState.anchor) {
8439
- clearSelection();
8440
- }
8441
- if (buffer.length > 0) {
8442
- buffer = "";
8443
- cursorPos = 0;
8444
- if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
8445
- emitCommandOutput(t("ctrlCClearedDraft"));
8446
- return;
8447
- }
8448
- if (lastCtrlCDoublePress !== null && now - lastCtrlCDoublePress <= 1e3) {
8449
- lastCtrlCDoublePress = null;
8450
- fixedInput.disable();
8451
- finish();
8452
- return;
8453
- }
8454
- lastCtrlCDoublePress = now;
8455
- emitCommandOutput(t("ctrlCExitHint"));
8593
+ await handleCtrlCKey(busyLock);
8456
8594
  return;
8457
8595
  }
8458
8596
  if (selectionState.anchor) {
@@ -8535,56 +8673,11 @@ ${merged}` : merged;
8535
8673
  const busySlashCommand = trimmedText.split(/\s+/)[0]?.toLowerCase();
8536
8674
  const isBusyLocalSlash = busySlashCommand === "/context" || busySlashCommand === "/ctx" || busySlashCommand === "/switch" || busySlashCommand === "/theme" || busySlashCommand === "/density" || busySlashCommand === "/runtime" || busySlashCommand === "/tools" || busySlashCommand === "/thinking" || busySlashCommand === "/auto" || busySlashCommand === "/tasks" || busySlashCommand === "/jobs" || busySlashCommand === "/procs" || busySlashCommand === "/agents" || busySlashCommand === "/doc" || busySlashCommand === "/skill" || busySlashCommand === "/customize" || busySlashCommand === "/login" || busySlashCommand === "/profile" || busySlashCommand === "/version";
8537
8675
  if (isBusyLocalSlash) {
8538
- releaseCollapsedPasteReferences(submittedText, pasteStore);
8539
- buffer = "";
8540
- cursorPos = 0;
8541
- if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
8542
- const beforeAgentKey = state.agentKey;
8543
- const res = handleTuiInput(submittedText, state);
8544
- if (res.action?.type === "theme-refresh") {
8545
- state = res.nextState;
8546
- const detected = await detectTerminalBackground({
8547
- stdin: input,
8548
- stdout: output,
8549
- allowSystemFallback: true
8550
- });
8551
- let refreshMsg = "";
8552
- if (detected && applyDetectedBackground(detected)) {
8553
- refreshMsg = t("themeRefreshed", detected.brightness);
8554
- } else if (detected) {
8555
- refreshMsg = t("themeRefreshed", detected.brightness);
8556
- } else {
8557
- refreshMsg = t("themeRefreshFailed");
8558
- }
8559
- if (refreshMsg) {
8560
- output.write(`${refreshMsg}
8561
- `);
8562
- }
8563
- } else if (res.action) {
8564
- output.write(
8565
- "Model picker isn't available while a reply is running. Use `/switch <name>` to switch now (takes effect on the next turn), or wait for the reply to finish.\n"
8566
- );
8567
- } else {
8568
- state = res.nextState;
8569
- let msg = res.output;
8570
- if (busySlashCommand === "/switch" && persistExplicitAgentSwitch(beforeAgentKey)) {
8571
- const hint = "Note: the new model takes effect on the next turn. Switching models may consume more tokens because the conversation context is re-sent to the new model.";
8572
- msg = msg ? `${msg}
8573
- ${hint}` : hint;
8574
- }
8575
- if (msg) {
8576
- output.write(`${msg}
8577
- `);
8578
- }
8579
- }
8580
- if (busySlashCommand === "/theme") {
8581
- renderHistoryToOutput();
8582
- if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
8583
- }
8676
+ await handleBusyLocalSlash(submittedText, busySlashCommand);
8584
8677
  return;
8585
8678
  }
8586
8679
  const { actionGateHandler, confirmDestructiveAction } = buildInteractiveTurnHandlers();
8587
- const binding = ensureChatQueueBinding(actionGateHandler, confirmDestructiveAction);
8680
+ const binding = ensureChatQueueBinding(turnCtx, actionGateHandler, confirmDestructiveAction);
8588
8681
  const decision = binding.resolveSubmit({
8589
8682
  text: submittedText,
8590
8683
  isRunning: true
@@ -8596,13 +8689,13 @@ ${hint}` : hint;
8596
8689
  fixedInput.repaint(buffer, cursorPos);
8597
8690
  } else if (decision.kind === "queue-blocked") {
8598
8691
  } else if (decision.kind === "noop" && !submittedText.trim() && binding.queueLength() > 0) {
8599
- preemptAndAbortForDrain(binding);
8692
+ preemptAndAbortForDrain(binding, activeTurnAbort);
8600
8693
  }
8601
8694
  return;
8602
8695
  }
8603
8696
  if (!submittedText.trim() && chatQueueBinding && chatQueueBinding.queueLength() > 0) {
8604
8697
  const { actionGateHandler, confirmDestructiveAction } = buildInteractiveTurnHandlers();
8605
- const manualBinding = ensureChatQueueBinding(actionGateHandler, confirmDestructiveAction);
8698
+ const manualBinding = ensureChatQueueBinding(turnCtx, actionGateHandler, confirmDestructiveAction);
8606
8699
  const drainedText = manualBinding.drainHeadForManualTurn();
8607
8700
  if (drainedText !== null) {
8608
8701
  buffer = "";