nolo-cli 0.37.0-alpha.2 → 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-NFM3Z5UX.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.2",
3
+ "version": "0.37.0-alpha.4",
4
4
  "type": "module",
5
5
  "description": "Agent-first terminal workspace for Nolo",
6
6
  "bin": {
@@ -4394,6 +4394,24 @@ async function readImagePaths(paths, options = {}) {
4394
4394
  }
4395
4395
  return { images, failures };
4396
4396
  }
4397
+ async function resolveAttachmentImageUrls({
4398
+ actionImagePaths,
4399
+ attachedImages,
4400
+ onFailure
4401
+ }) {
4402
+ const pathsToRead = [
4403
+ ...actionImagePaths ?? [],
4404
+ ...attachedImages.map((img) => img.sourcePath)
4405
+ ];
4406
+ let imageUrls = [];
4407
+ if (pathsToRead.length > 0) {
4408
+ const readResult = await readImagePaths(pathsToRead, {
4409
+ onFailure
4410
+ });
4411
+ imageUrls = readResult.images.map((img) => img.dataUrl);
4412
+ }
4413
+ return { imageUrls };
4414
+ }
4397
4415
 
4398
4416
  // packages/cli/tui/clipboardImage.ts
4399
4417
  import { createHash } from "node:crypto";
@@ -7076,6 +7094,219 @@ function waitForRawActionGate(input, output, gate, spawnRunner, hooks) {
7076
7094
  }
7077
7095
  });
7078
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
+ }
7079
7310
  var latestWorkspaceThemeOwner = 0;
7080
7311
  async function runTuiWorkspace(options) {
7081
7312
  initCliLocale(options.env ?? process.env);
@@ -7355,220 +7586,6 @@ ${content}`);
7355
7586
  });
7356
7587
  };
7357
7588
  let composerDecoderDrain = null;
7358
- const runOneAgentTurn = async (inputMsg, imageUrls, actionGateHandler, confirmDestructiveAction) => {
7359
- const scheduleTitlePatchSync = (runResult) => {
7360
- if (!runResult.titlePatchPromise || !runResult.dialogId) return;
7361
- const patchDialogId = runResult.dialogId;
7362
- runResult.titlePatchPromise.then((patchedTitle) => {
7363
- if (!patchedTitle || sessionEnded) return;
7364
- if (state.dialogId !== patchDialogId) return;
7365
- if (state.dialogLabel === patchedTitle) return;
7366
- state = {
7367
- ...state,
7368
- dialogLabel: patchedTitle,
7369
- dialogTitle: patchedTitle
7370
- };
7371
- syncWindowTitle();
7372
- }).catch(() => {
7373
- });
7374
- };
7375
- let req = createTurnRequest(inputMsg);
7376
- if (req.event.kind === "child-run-completed") {
7377
- const nowMs = Date.now();
7378
- const { remainingRuns, claimedRunIds } = filterUnclaimedChildRuns(
7379
- req.event.runs,
7380
- { env: effectiveEnv, now: () => nowMs }
7381
- );
7382
- for (const runId of claimedRunIds) {
7383
- runCompletionWatcher.markAcknowledged(runId);
7384
- }
7385
- if (remainingRuns.length === 0) {
7386
- for (const r of req.event.runs) {
7387
- runCompletionWatcher.markAcknowledged(r.runId);
7388
- }
7389
- return { ok: true, aborted: false };
7390
- }
7391
- for (const r of remainingRuns) {
7392
- runCompletionWatcher.markAcknowledged(r.runId);
7393
- }
7394
- if (remainingRuns.length !== req.event.runs.length) {
7395
- const text = buildWakeMessage(remainingRuns, nowMs);
7396
- const displayText = buildWakeDisplayText(remainingRuns, nowMs);
7397
- const updatedEvent = {
7398
- kind: "child-run-completed",
7399
- runs: remainingRuns,
7400
- text,
7401
- displayText
7402
- };
7403
- req = {
7404
- text,
7405
- event: updatedEvent
7406
- };
7407
- }
7408
- }
7409
- const message = req.text;
7410
- forcedStop = false;
7411
- turnEpoch += 1;
7412
- const myEpoch = turnEpoch;
7413
- history.followBottom = true;
7414
- const isInternalEvent = req.event.kind !== "user";
7415
- const transcriptText = req.event.kind === "child-run-completed" ? req.event.displayText ?? req.event.text : message;
7416
- startTurn(history, isInternalEvent ? "assistant" : "user");
7417
- appendToCurrentTurn(
7418
- history,
7419
- isInternalEvent ? dimCliText(transcriptText, resolveCliColorEnabled()) : transcriptText
7420
- );
7421
- finalizeCurrentTurn(history);
7422
- renderHistoryToOutput();
7423
- if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
7424
- startTurn(history, "assistant");
7425
- const agentOutput = isInteractiveInput(input) ? createHistoryOutputStream(history, () => {
7426
- scheduleRender();
7427
- }) : output;
7428
- const requestUserChoice = isInteractiveInput(input) && dialogHost ? async (req2) => {
7429
- modalOwnsKeyboard = true;
7430
- try {
7431
- return await dialogHost.run(
7432
- (anchor) => runAskChoiceDialog({
7433
- request: req2,
7434
- input,
7435
- output,
7436
- ...anchor
7437
- })
7438
- );
7439
- } catch {
7440
- return { kind: "cancelled" };
7441
- } finally {
7442
- composerDecoderDrain?.();
7443
- modalOwnsKeyboard = false;
7444
- }
7445
- } : void 0;
7446
- try {
7447
- activeTurnAbort = new AbortController();
7448
- activeTurnEpoch = myEpoch;
7449
- const runResult = await runAgentChat(
7450
- options.scriptDir,
7451
- state,
7452
- message,
7453
- options.env ?? process.env,
7454
- agentOutput,
7455
- options.agentRunner,
7456
- {
7457
- ...imageUrls.length > 0 ? { imageUrls } : {},
7458
- actionGateHandler,
7459
- ...confirmDestructiveAction ? { confirmDestructiveAction } : {},
7460
- ...requestUserChoice ? { requestUserChoice } : {},
7461
- abortSignal: activeTurnAbort.signal,
7462
- pastedTextStore: pasteStore,
7463
- activityReporter,
7464
- onAgentRunStatus: (snapshot) => {
7465
- if (snapshot) {
7466
- activityIndicator.updateAgentRun(snapshot);
7467
- runRegistryPoller.ensureRunning();
7468
- } else {
7469
- activityIndicator.clearAgentRun();
7470
- }
7471
- }
7472
- }
7473
- );
7474
- const wasForceStopped = forcedStopEpoch === myEpoch;
7475
- if (wasForceStopped) {
7476
- if (runResult.dialogId || runResult.turnTokens) {
7477
- const nextDialogKey = runResult.dialogId ? runResult.dialogId === state.dialogId && state.dialogKey ? state.dialogKey : state.dialogOwnerId ? `dialog-${state.dialogOwnerId}-${runResult.dialogId}` : void 0 : state.dialogKey;
7478
- state = {
7479
- ...state,
7480
- ...runResult.dialogId ? {
7481
- dialogId: runResult.dialogId,
7482
- dialogKey: nextDialogKey,
7483
- dialogLabel: runResult.title || runResult.dialogId,
7484
- ...runResult.title ? { dialogTitle: runResult.title } : {}
7485
- } : {},
7486
- ...runResult.turnTokens ? { turnTokens: runResult.turnTokens } : {},
7487
- ...runResult.cachedMemoryOverlay !== void 0 ? { cachedMemoryOverlay: runResult.cachedMemoryOverlay } : {}
7488
- };
7489
- if (runResult.dialogId && nextDialogKey) {
7490
- refreshDialogTotalCredits(runResult.dialogId, nextDialogKey);
7491
- }
7492
- }
7493
- scheduleTitlePatchSync(runResult);
7494
- return { ok: false, aborted: true };
7495
- }
7496
- const wasAborted = activeTurnAbort.signal.aborted;
7497
- activeTurnAbort = null;
7498
- if (shouldEmitTerminalBell({
7499
- wasAborted,
7500
- streamInterrupted: runResult.streamInterrupted,
7501
- exitCode: runResult.exitCode,
7502
- interactive: isInteractiveInput(input)
7503
- })) {
7504
- emitTerminalBell(output);
7505
- }
7506
- if (isInteractiveInput(input)) {
7507
- finalizeCurrentTurn(history);
7508
- flushPendingRender();
7509
- renderHistoryToOutput();
7510
- if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
7511
- }
7512
- if (wasAborted) {
7513
- if (runResult.pendingToolName) {
7514
- emitCommandOutput(t("turnStoppedToolPending", runResult.pendingToolName));
7515
- } else {
7516
- emitCommandOutput(t("turnStopped"));
7517
- }
7518
- }
7519
- if (runResult.dialogId || runResult.turnTokens || runResult.contextWindow) {
7520
- const nextDialogKey = runResult.dialogId ? runResult.dialogId === state.dialogId && state.dialogKey ? state.dialogKey : state.dialogOwnerId ? `dialog-${state.dialogOwnerId}-${runResult.dialogId}` : void 0 : state.dialogKey;
7521
- state = {
7522
- ...state,
7523
- ...runResult.dialogId ? {
7524
- dialogId: runResult.dialogId,
7525
- dialogKey: nextDialogKey,
7526
- dialogLabel: runResult.title || runResult.dialogId,
7527
- ...runResult.title ? { dialogTitle: runResult.title } : {}
7528
- } : {},
7529
- ...runResult.turnTokens ? { turnTokens: runResult.turnTokens } : {},
7530
- ...runResult.contextWindow ? { contextWindow: runResult.contextWindow } : {},
7531
- // input_tokens 是累计上下文输入(含历史消息),把它持久化到
7532
- // estimatedContextTokens:下一轮若 provider 不返回 usage,context
7533
- // chip 仍显示真实累计占用而不是回退到启动时的静态估算。
7534
- ...runResult.turnTokens && runResult.turnTokens.input > 0 ? { estimatedContextTokens: runResult.turnTokens.input } : {},
7535
- ...runResult.cachedMemoryOverlay !== void 0 ? { cachedMemoryOverlay: runResult.cachedMemoryOverlay } : {}
7536
- };
7537
- if (runResult.dialogId && nextDialogKey) {
7538
- refreshDialogTotalCredits(runResult.dialogId, nextDialogKey);
7539
- }
7540
- }
7541
- scheduleTitlePatchSync(runResult);
7542
- if (!wasAborted && runResult.exitCode !== 0) {
7543
- if (isBalanceExhaustedError(runResult.localError) && runResult.dialogId) {
7544
- emitCommandOutput(t("balanceExhaustedHint"));
7545
- } else if (isQuotaExhaustedError(runResult.localError)) {
7546
- emitCommandOutput(t("quotaExhaustedHint"));
7547
- } else if (runResult.dialogId) {
7548
- emitCommandOutput(t("dialogPreservedHint"));
7549
- } else {
7550
- emitCommandOutput(t("dialogNotSavedHint"));
7551
- }
7552
- }
7553
- return { ok: !wasAborted, aborted: wasAborted };
7554
- } finally {
7555
- activityIndicator.stop();
7556
- activeTurnAbort = null;
7557
- }
7558
- };
7559
- let chatQueueBinding = null;
7560
- const ensureChatQueueBinding = (actionGateHandler, confirmDestructiveAction) => {
7561
- if (chatQueueBinding) return chatQueueBinding;
7562
- chatQueueBinding = createChatQueueTuiBinding(async (text) => {
7563
- return runOneAgentTurn(text, [], actionGateHandler, confirmDestructiveAction);
7564
- });
7565
- return chatQueueBinding;
7566
- };
7567
- const preemptAndAbortForDrain = (binding) => {
7568
- if (binding.preemptForDrain() && activeTurnAbort) {
7569
- activeTurnAbort.abort();
7570
- }
7571
- };
7572
7589
  const emitCommandOutput = (text, command = "") => {
7573
7590
  if (!text) return;
7574
7591
  if (!isInteractiveInput(input)) {
@@ -7581,6 +7598,92 @@ ${content}`);
7581
7598
  renderHistoryToOutput();
7582
7599
  if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
7583
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
+ };
7584
7687
  const persistExplicitAgentSwitch = (previousAgentKey) => {
7585
7688
  if (state.agentKey === previousAgentKey) return false;
7586
7689
  persistAgentSelection(
@@ -7596,8 +7699,9 @@ ${content}`);
7596
7699
  const previousAgentKey = state.agentKey;
7597
7700
  state = result.nextState;
7598
7701
  const interactive = isInteractiveInput(input);
7599
- if (result.action?.type !== "chat" && result.action?.type !== "exit" && result.output) {
7600
- emitCommandOutput(result.output, interactive ? line.trim() : "");
7702
+ if (result.action?.type !== "exit" && result.output) {
7703
+ const shouldEmit = result.action?.type !== "chat" || !interactive;
7704
+ if (shouldEmit) emitCommandOutput(result.output, interactive ? line.trim() : "");
7601
7705
  }
7602
7706
  if (state.agentKey !== previousAgentKey) {
7603
7707
  persistExplicitAgentSwitch(previousAgentKey);
@@ -7990,27 +8094,22 @@ ${stderrText.trim()}
7990
8094
  }
7991
8095
  }
7992
8096
  if (result.action?.type === "chat") {
7993
- const pathsToRead = [
7994
- ...result.action.imagePaths ?? [],
7995
- ...state.attachedImages.map((img) => img.sourcePath)
7996
- ];
7997
- let imageUrls = [];
7998
- if (pathsToRead.length > 0) {
7999
- const readResult = await readImagePaths(pathsToRead, {
8000
- onFailure: (_path, err) => output.write(`[nolo] image skipped: ${err.message}
8097
+ const { imageUrls } = await resolveAttachmentImageUrls({
8098
+ actionImagePaths: result.action.imagePaths,
8099
+ attachedImages: state.attachedImages,
8100
+ onFailure: (_path, err) => output.write(`[nolo] image skipped: ${err.message}
8001
8101
  `)
8002
- });
8003
- imageUrls = readResult.images.map((img) => img.dataUrl);
8004
- }
8102
+ });
8005
8103
  state = {
8006
8104
  ...state,
8007
8105
  attachedImages: []
8008
8106
  };
8009
8107
  history.followBottom = true;
8010
- const binding = ensureChatQueueBinding(actionGateHandler, confirmDestructiveAction);
8108
+ const binding = ensureChatQueueBinding(turnCtx, actionGateHandler, confirmDestructiveAction);
8011
8109
  binding.notifyTurnStart();
8012
8110
  try {
8013
8111
  const outcome = await runOneAgentTurn(
8112
+ turnCtx,
8014
8113
  result.action.message,
8015
8114
  imageUrls,
8016
8115
  actionGateHandler,
@@ -8169,12 +8268,13 @@ ${err.message}` : ""}`
8169
8268
  const runIdleTextTurn = async (inputMsg) => {
8170
8269
  const req = createTurnRequest(inputMsg);
8171
8270
  const { actionGateHandler, confirmDestructiveAction } = buildInteractiveTurnHandlers();
8172
- const binding = ensureChatQueueBinding(actionGateHandler, confirmDestructiveAction);
8271
+ const binding = ensureChatQueueBinding(turnCtx, actionGateHandler, confirmDestructiveAction);
8173
8272
  binding.notifyTurnStart();
8174
8273
  busy = true;
8175
8274
  fixedInput.enterOutputMode(req.text);
8176
8275
  try {
8177
8276
  const outcome = await runOneAgentTurn(
8277
+ turnCtx,
8178
8278
  req,
8179
8279
  [],
8180
8280
  actionGateHandler,
@@ -8198,7 +8298,7 @@ ${err.message}` : ""}`
8198
8298
  if (done) return;
8199
8299
  if (busy || fixedInput.isPaused()) {
8200
8300
  const { actionGateHandler, confirmDestructiveAction } = buildInteractiveTurnHandlers();
8201
- ensureChatQueueBinding(actionGateHandler, confirmDestructiveAction).enqueue(event);
8301
+ ensureChatQueueBinding(turnCtx, actionGateHandler, confirmDestructiveAction).enqueue(event);
8202
8302
  if (fixedInput.active && !fixedInput.isPaused()) {
8203
8303
  fixedInput.repaint(buffer, cursorPos);
8204
8304
  }
@@ -8258,6 +8358,109 @@ ${err.message}` : ""}`
8258
8358
  paintFrame(buffer);
8259
8359
  }, 60);
8260
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
+ };
8261
8464
  const handleInputToken = async (sequence) => {
8262
8465
  if (done) return;
8263
8466
  if (modalOwnsKeyboard) {
@@ -8280,7 +8483,7 @@ ${merged}` : merged;
8280
8483
  t(busy ? "flushQueuedBusyHint" : "flushQueuedIdleHint", String(totalCount))
8281
8484
  );
8282
8485
  if (busy) {
8283
- preemptAndAbortForDrain(chatQueueBinding);
8486
+ preemptAndAbortForDrain(chatQueueBinding, activeTurnAbort);
8284
8487
  return;
8285
8488
  }
8286
8489
  buffer = "";
@@ -8387,59 +8590,7 @@ ${merged}` : merged;
8387
8590
  return;
8388
8591
  }
8389
8592
  if (sequence === "") {
8390
- if (busyLock && activeTurnAbort) {
8391
- const stopBinding = chatQueueBinding;
8392
- if (stopBinding && stopBinding.queueLength() > 0) {
8393
- stopBinding.preemptForStop();
8394
- }
8395
- activityIndicator.markStopping();
8396
- activeTurnAbort.abort();
8397
- return;
8398
- }
8399
- const now = Date.now();
8400
- const hasSelection = selectionState.anchor !== null && selectionState.head !== null && !areSelectionPointsEqual(selectionState.anchor, selectionState.head);
8401
- if (hasSelection) {
8402
- const tty = output;
8403
- const columns = tty.columns ?? 80;
8404
- const contentWidth = Math.max(1, columns - 1);
8405
- const textToCopy = extractSelectedText(
8406
- history,
8407
- selectionState.anchor,
8408
- selectionState.head,
8409
- contentWidth
8410
- );
8411
- clearSelection();
8412
- if (textToCopy.length > 0) {
8413
- try {
8414
- await writeClipboard2(textToCopy);
8415
- emitCommandOutput(t("copiedSelection"));
8416
- } catch (error) {
8417
- emitCommandOutput(
8418
- `[nolo] ${t("copyFailed")}: ${toErrorMessage(error)}`
8419
- );
8420
- }
8421
- }
8422
- paintFrame(buffer);
8423
- return;
8424
- }
8425
- if (selectionState.anchor) {
8426
- clearSelection();
8427
- }
8428
- if (buffer.length > 0) {
8429
- buffer = "";
8430
- cursorPos = 0;
8431
- if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
8432
- emitCommandOutput(t("ctrlCClearedDraft"));
8433
- return;
8434
- }
8435
- if (lastCtrlCDoublePress !== null && now - lastCtrlCDoublePress <= 1e3) {
8436
- lastCtrlCDoublePress = null;
8437
- fixedInput.disable();
8438
- finish();
8439
- return;
8440
- }
8441
- lastCtrlCDoublePress = now;
8442
- emitCommandOutput(t("ctrlCExitHint"));
8593
+ await handleCtrlCKey(busyLock);
8443
8594
  return;
8444
8595
  }
8445
8596
  if (selectionState.anchor) {
@@ -8522,56 +8673,11 @@ ${merged}` : merged;
8522
8673
  const busySlashCommand = trimmedText.split(/\s+/)[0]?.toLowerCase();
8523
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";
8524
8675
  if (isBusyLocalSlash) {
8525
- releaseCollapsedPasteReferences(submittedText, pasteStore);
8526
- buffer = "";
8527
- cursorPos = 0;
8528
- if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
8529
- const beforeAgentKey = state.agentKey;
8530
- const res = handleTuiInput(submittedText, state);
8531
- if (res.action?.type === "theme-refresh") {
8532
- state = res.nextState;
8533
- const detected = await detectTerminalBackground({
8534
- stdin: input,
8535
- stdout: output,
8536
- allowSystemFallback: true
8537
- });
8538
- let refreshMsg = "";
8539
- if (detected && applyDetectedBackground(detected)) {
8540
- refreshMsg = t("themeRefreshed", detected.brightness);
8541
- } else if (detected) {
8542
- refreshMsg = t("themeRefreshed", detected.brightness);
8543
- } else {
8544
- refreshMsg = t("themeRefreshFailed");
8545
- }
8546
- if (refreshMsg) {
8547
- output.write(`${refreshMsg}
8548
- `);
8549
- }
8550
- } else if (res.action) {
8551
- output.write(
8552
- "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"
8553
- );
8554
- } else {
8555
- state = res.nextState;
8556
- let msg = res.output;
8557
- if (busySlashCommand === "/switch" && persistExplicitAgentSwitch(beforeAgentKey)) {
8558
- 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.";
8559
- msg = msg ? `${msg}
8560
- ${hint}` : hint;
8561
- }
8562
- if (msg) {
8563
- output.write(`${msg}
8564
- `);
8565
- }
8566
- }
8567
- if (busySlashCommand === "/theme") {
8568
- renderHistoryToOutput();
8569
- if (fixedInput.active) fixedInput.repaint(buffer, cursorPos);
8570
- }
8676
+ await handleBusyLocalSlash(submittedText, busySlashCommand);
8571
8677
  return;
8572
8678
  }
8573
8679
  const { actionGateHandler, confirmDestructiveAction } = buildInteractiveTurnHandlers();
8574
- const binding = ensureChatQueueBinding(actionGateHandler, confirmDestructiveAction);
8680
+ const binding = ensureChatQueueBinding(turnCtx, actionGateHandler, confirmDestructiveAction);
8575
8681
  const decision = binding.resolveSubmit({
8576
8682
  text: submittedText,
8577
8683
  isRunning: true
@@ -8583,13 +8689,13 @@ ${hint}` : hint;
8583
8689
  fixedInput.repaint(buffer, cursorPos);
8584
8690
  } else if (decision.kind === "queue-blocked") {
8585
8691
  } else if (decision.kind === "noop" && !submittedText.trim() && binding.queueLength() > 0) {
8586
- preemptAndAbortForDrain(binding);
8692
+ preemptAndAbortForDrain(binding, activeTurnAbort);
8587
8693
  }
8588
8694
  return;
8589
8695
  }
8590
8696
  if (!submittedText.trim() && chatQueueBinding && chatQueueBinding.queueLength() > 0) {
8591
8697
  const { actionGateHandler, confirmDestructiveAction } = buildInteractiveTurnHandlers();
8592
- const manualBinding = ensureChatQueueBinding(actionGateHandler, confirmDestructiveAction);
8698
+ const manualBinding = ensureChatQueueBinding(turnCtx, actionGateHandler, confirmDestructiveAction);
8593
8699
  const drainedText = manualBinding.drainHeadForManualTurn();
8594
8700
  if (drainedText !== null) {
8595
8701
  buffer = "";