newmark-agent 0.4.3 → 0.4.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.
@@ -336405,7 +336405,7 @@ var ToolExecutor = class {
336405
336405
  t3("subagent_result", "Return the persisted transcript, mailbox summary, status, and latest result for a peer agent. Target by exact id (preferred) or name.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name." } }, []),
336406
336406
  t3("subagent_close", "Close a same-conversation peer. Root can close any peer; a peer can close only itself. Target by exact id (preferred) or name.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name." } }, []),
336407
336407
  t3("linked_plan", "Read or update the current conversation linked Markdown plan. Update requires the current expected_revision.", { action: { type: "string", enum: ["get", "update"] }, markdown: { type: "string" }, expected_revision: { type: "number" } }, ["action"]),
336408
- t3("build_history_query", "Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query. Every activity/guide content is bounded to max_chars (default 2000) to keep the read lean and cache-friendly.", { history_index: { type: "number", minimum: 1, description: "1-based historical Build Block index from the request ledger; 1 is the newest previous task." }, run_id: { type: "string", description: "Exact run id returned by an earlier build_history_query result." }, max_events: { type: "number", minimum: 1, maximum: 200, description: "Maximum trailing public work events; defaults to 80." }, max_chars: { type: "number", minimum: 100, maximum: 4e3, description: "Per-event/per-guide content character bound; defaults to 2000." } }, []),
336408
+ t3("build_history_query", "Read the concrete public work details (tool calls, results, file changes, guides) of one historical Build Block. Call it proactively when the current task continues, fixes, verifies, or depends on earlier work: reuse the returned activity instead of re-investigating from scratch. Do not call it merely to answer completion status already exposed by the prompt. Select by newest-to-oldest history_index, or by run_id returned from an earlier query. Every activity/guide content is bounded to max_chars (default 2000) to keep the read lean and cache-friendly.", { history_index: { type: "number", minimum: 1, description: "1-based historical Build Block index from the request ledger; 1 is the newest previous task." }, run_id: { type: "string", description: "Exact run id returned by an earlier build_history_query result." }, max_events: { type: "number", minimum: 1, maximum: 200, description: "Maximum trailing public work events; defaults to 80." }, max_chars: { type: "number", minimum: 100, maximum: 4e3, description: "Per-event/per-guide content character bound; defaults to 2000." } }, []),
336409
336409
  t3("context_compress", "Actively compress the LLM context history for this conversation. This collapses older history entries into a concise summary while preserving the recent tail, which reduces context tokens and cost. IMPORTANT: it affects only the LLM context (what the model sees); the displayed conversation history shown to the user is never altered. Call this when the conversation is long, token pressure is high, or you judge that older turns are no longer needed in full. Idempotent and safe: repeated calls produce incremental summaries.", { keep_recent: { type: "number", minimum: 2, maximum: 60, description: "Recent message count to keep uncompressed at the tail. Defaults to the configured keep_recent_messages." }, force: { type: "boolean", description: "Compress even if the context is not yet over the automatic threshold. Defaults to false." } }, []),
336410
336410
  t3("context_history_manage", "Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. The hot cache stays bounded; evicted folded segments remain in a conversation-isolated append-only cold archive and are loaded only by explicit search/read/restore calls. Actions: list returns a bounded index of current context entries; remove declares one long-term entry for unload (see below); summarize folds a contiguous current range; restore reinserts a folded segment when its summary marker is still present; search finds matching hot or archived segments; read returns one bounded segment without injecting the whole archive; status reports budgets, hot cache, cold archive, the protected recent zone, and pending removals. The recent context tail and last user message are protected from remove/summarize unless dangerous is true. For cache-optimization, remove ONLY targets long-term history (never the protected recent tail or last user message) and does NOT unload immediately: the declared entry stays in context for the rest of the current Build Block so the provider prefix cache stays stable, then is physically removed when the Block ends \u2014 applying to subsequent Blocks only.", {
336411
336411
  action: { type: "string", enum: ["list", "remove", "summarize", "restore", "search", "read", "status"], description: "list current entries; remove one; summarize a range; restore by restore_id; search hot/cold folded segments; read one bounded folded segment; status report context budgets and storage." },
@@ -340900,7 +340900,7 @@ function buildConversationTaskLedger(agent) {
340900
340900
  "Unfinished Continuation Queue (newest to oldest; summary fields only; use only when the current user instruction authorizes continuation and the task is relevant):",
340901
340901
  ...unfinishedLines.length ? unfinishedLines : ["(none)"],
340902
340902
  ...unfinished.length > unfinishedLines.length ? [`(${unfinished.length - unfinishedLines.length} older unfinished run(s) omitted from the bounded prompt ledger.)`] : [],
340903
- "When concrete work details are required, call build_history_query with history_index from this list. Do not call it merely to answer completion status already shown here."
340903
+ "When the current task continues, fixes, verifies, or depends on earlier work in this list, proactively call build_history_query with its history_index before re-investigating. Reuse the returned tool activity and results instead of re-running commands or re-reading files this conversation already examined. Querying history is read-only and never authorizes resuming that work; do not query merely to answer completion status already shown here."
340904
340904
  ].join("\n");
340905
340905
  }
340906
340906
  async function shouldStopAfterTurn(agent, message) {
@@ -349168,8 +349168,13 @@ ${summary}`, segment, "local-summarize", true);
349168
349168
  };
349169
349169
  }
349170
349170
  resolveWindowModel(modelName) {
349171
- if (modelName !== "auto") return this.config.findModel(modelName);
349172
- return this.activeModelConfig() || this.config.findModel(this.config.getStr("models", "default_model"));
349171
+ if (modelName === "auto" || modelName === this.model || modelName === this.activeModelName()) {
349172
+ const active = this.activeModelConfig();
349173
+ if (active) return active;
349174
+ }
349175
+ const byName = this.config.findModel(modelName);
349176
+ if (byName) return byName;
349177
+ return this.config.findModel(this.config.getStr("models", "default_model"));
349173
349178
  }
349174
349179
  contextMaxTokens(modelName = this.model) {
349175
349180
  const model = this.resolveWindowModel(modelName);
@@ -352333,7 +352338,7 @@ ${custom}`);
352333
352338
  "- Memory Lab is governed by an explicit Policy chain: pre-think whether memory is needed; prefer bounded memory_lab_query retrieval; then choose ADD/UPDATE/DELETE only when the user authorizes durable memory mutation.",
352334
352339
  "- Before memory_lab_update, inspect the target with memory_lab_query or memory_lab_read. For an existing component pass expectedUpdatedAt so concurrent/stale writes fail closed; preserve established tag parent paths.",
352335
352340
  "- Use memory_lab_delete only for an explicit user request to forget/remove memory. Prior revisions are retained under Memory Lab/archive and mutation decisions are appended to policy.jsonl for replay.",
352336
- "- Build history disclosure is two-layered. The request prompt contains only each historical Build Block user input, final summary, and completion status. Use build_history_query only when the current user asks what specifically happened in one Build Block; querying history is read-only and never authorizes resuming that work.",
352341
+ "- Build history disclosure is two-layered. The request prompt contains only each historical Build Block user input, final summary, and completion status. When the current task continues, fixes, verifies, or depends on earlier Build Blocks, proactively call build_history_query to read the concrete tool activity and results of the relevant block, and reuse that information instead of re-investigating (re-running commands or re-reading files) from scratch. Querying history is read-only and never authorizes resuming that work; do not query merely to answer completion status already shown in the prompt.",
352337
352342
  "- Linked plan disclosure: a durable conversation-linked Markdown plan exists and can be inspected or updated with linked_plan when explicitly needed or required by Plan mode. Its full Markdown and revision are not injected into every model request.",
352338
352343
  "- A memory_lab_update, memory_lab_delete, or memory_lab_reindex call is unfinished until its awaited tool result contains rebuildReceipt.completed=true. The completion receipt is represented by the tool activity inside the current Build block and should not be repeated as a separate completion message.",
352339
352344
  `- Skills and subagents: skill searches enabled metadata and loads one SKILL.md body on demand; skill_download installs offline skill folders; task creates constrained subagents tracked in agent state.`,
@@ -353131,6 +353136,18 @@ var ConversationKernel = class {
353131
353136
  if (runtime) runtime.options.mode = mode;
353132
353137
  return runner.mode;
353133
353138
  }
353139
+ setModel(target, model) {
353140
+ const normalized = this.normalizeTarget(target);
353141
+ const runtime = this.findRuntime(normalized);
353142
+ const runner = runtime?.runner || this.createRunner(normalized);
353143
+ if (!runtime || !runtime.activePromise) {
353144
+ runner.setModel(model);
353145
+ } else {
353146
+ runtime.options.model = model;
353147
+ }
353148
+ runner.saveWorkspaceConversationState(true);
353149
+ return runner.model;
353150
+ }
353134
353151
  async toggleGoalPause(target) {
353135
353152
  const normalized = this.normalizeTarget(target);
353136
353153
  let runtime = this.findRuntime(normalized);
@@ -353245,14 +353262,15 @@ var ConversationKernel = class {
353245
353262
  }
353246
353263
  async prompt(message, target, options, queueMode = "followUp") {
353247
353264
  const normalized = this.normalizeTarget(target);
353265
+ const active = this.findRuntime(normalized);
353266
+ if (active?.activePromise) {
353267
+ this.enqueueSameSession(active, message, queueMode);
353268
+ this.activateAcceptedGoal(active, typeof message === "string" ? "" : message.goalObjective);
353269
+ return active.activePromise;
353270
+ }
353248
353271
  const runtime = this.runtime(normalized, options);
353249
353272
  runtime.options = { ...options };
353250
353273
  this.applyOptions(runtime.runner, options);
353251
- if (runtime.activePromise) {
353252
- this.enqueueSameSession(runtime, message, queueMode);
353253
- this.activateAcceptedGoal(runtime, typeof message === "string" ? "" : message.goalObjective);
353254
- return runtime.activePromise;
353255
- }
353256
353274
  runtime.generation = (this.generations.get(runtime.runtimeKey) || runtime.generation || 0) + 1;
353257
353275
  this.generations.set(runtime.runtimeKey, runtime.generation);
353258
353276
  const requestedRunId = typeof message === "string" ? "" : String(message.runId || "").trim().slice(0, 200);
@@ -353397,7 +353415,21 @@ Review this persisted peer result and summarize or continue the parent task as n
353397
353415
  this.mirrorHostIfTargetActive(runtime);
353398
353416
  return this.result(runtime, lastTokens);
353399
353417
  }
353418
+ /**
353419
+ * Apply a model selection recorded while a Build block was running. The
353420
+ * in-flight block never switches mid-block; the switch takes effect the next
353421
+ * time a queued Guide/Next re-enters the block, and only when the pending
353422
+ * selection actually differs from the runner's current selection.
353423
+ */
353424
+ syncPendingModel(runtime) {
353425
+ const pending3 = String(runtime.options.model || "").trim();
353426
+ if (!pending3) return;
353427
+ if (pending3 === runtime.runner.model || pending3 === runtime.runner.modelSelectionValue()) return;
353428
+ runtime.runner.setModel(pending3);
353429
+ runtime.options.model = runtime.runner.modelSelectionValue();
353430
+ }
353400
353431
  async runSingle(runtime, message, continuationMode) {
353432
+ this.syncPendingModel(runtime);
353401
353433
  this.consumeQueuedMessage(runtime, typeof message === "string" ? message : message.text);
353402
353434
  const timeoutMs = this.processTimeoutMs(runtime);
353403
353435
  if (timeoutMs <= 0) {
@@ -354038,6 +354070,9 @@ async function handle(request) {
354038
354070
  if (request.method === "set_mode") {
354039
354071
  return kernel.setMode(checkedTarget(request.params.target), request.params.mode);
354040
354072
  }
354073
+ if (request.method === "set_model") {
354074
+ return kernel.setModel(checkedTarget(request.params.target), request.params.model);
354075
+ }
354041
354076
  if (request.method === "set_input_mode") {
354042
354077
  return kernel.setInputMode(checkedTarget(request.params.target), request.params.mode);
354043
354078
  }
@@ -93,6 +93,9 @@ async function handle(request) {
93
93
  if (request.method === 'set_mode') {
94
94
  return kernel.setMode(checkedTarget(request.params.target), request.params.mode);
95
95
  }
96
+ if (request.method === 'set_model') {
97
+ return kernel.setModel(checkedTarget(request.params.target), request.params.model);
98
+ }
96
99
  if (request.method === 'set_input_mode') {
97
100
  return kernel.setInputMode(checkedTarget(request.params.target), request.params.mode);
98
101
  }
@@ -5305,9 +5305,21 @@ class Agent {
5305
5305
  };
5306
5306
  }
5307
5307
  resolveWindowModel(modelName) {
5308
- if (modelName !== 'auto')
5309
- return this.config.findModel(modelName);
5310
- return this.activeModelConfig() || this.config.findModel(this.config.getStr('models', 'default_model'));
5308
+ // The context window (display ring, inspector, and compaction trigger) must
5309
+ // resolve the deployment that is actually running. For the active selection
5310
+ // auto or a fixed model — resolve through the active deployment so a
5311
+ // qualified selection or two same-named models across providers never fall
5312
+ // through to the 128000 default. Only a caller-supplied foreign name (for
5313
+ // example a validation probe against another model) resolves by bare name.
5314
+ if (modelName === 'auto' || modelName === this.model || modelName === this.activeModelName()) {
5315
+ const active = this.activeModelConfig();
5316
+ if (active)
5317
+ return active;
5318
+ }
5319
+ const byName = this.config.findModel(modelName);
5320
+ if (byName)
5321
+ return byName;
5322
+ return this.config.findModel(this.config.getStr('models', 'default_model'));
5311
5323
  }
5312
5324
  contextMaxTokens(modelName = this.model) {
5313
5325
  const model = this.resolveWindowModel(modelName);
@@ -8858,7 +8870,7 @@ class Agent {
8858
8870
  '- Memory Lab is governed by an explicit Policy chain: pre-think whether memory is needed; prefer bounded memory_lab_query retrieval; then choose ADD/UPDATE/DELETE only when the user authorizes durable memory mutation.',
8859
8871
  '- Before memory_lab_update, inspect the target with memory_lab_query or memory_lab_read. For an existing component pass expectedUpdatedAt so concurrent/stale writes fail closed; preserve established tag parent paths.',
8860
8872
  '- Use memory_lab_delete only for an explicit user request to forget/remove memory. Prior revisions are retained under Memory Lab/archive and mutation decisions are appended to policy.jsonl for replay.',
8861
- '- Build history disclosure is two-layered. The request prompt contains only each historical Build Block user input, final summary, and completion status. Use build_history_query only when the current user asks what specifically happened in one Build Block; querying history is read-only and never authorizes resuming that work.',
8873
+ '- Build history disclosure is two-layered. The request prompt contains only each historical Build Block user input, final summary, and completion status. When the current task continues, fixes, verifies, or depends on earlier Build Blocks, proactively call build_history_query to read the concrete tool activity and results of the relevant block, and reuse that information instead of re-investigating (re-running commands or re-reading files) from scratch. Querying history is read-only and never authorizes resuming that work; do not query merely to answer completion status already shown in the prompt.',
8862
8874
  '- Linked plan disclosure: a durable conversation-linked Markdown plan exists and can be inspected or updated with linked_plan when explicitly needed or required by Plan mode. Its full Markdown and revision are not injected into every model request.',
8863
8875
  '- A memory_lab_update, memory_lab_delete, or memory_lab_reindex call is unfinished until its awaited tool result contains rebuildReceipt.completed=true. The completion receipt is represented by the tool activity inside the current Build block and should not be repeated as a separate completion message.',
8864
8876
  `- Skills and subagents: skill searches enabled metadata and loads one SKILL.md body on demand; skill_download installs offline skill folders; task creates constrained subagents tracked in agent state.`,
@@ -818,7 +818,7 @@ function buildConversationTaskLedger(agent) {
818
818
  'Unfinished Continuation Queue (newest to oldest; summary fields only; use only when the current user instruction authorizes continuation and the task is relevant):',
819
819
  ...(unfinishedLines.length ? unfinishedLines : ['(none)']),
820
820
  ...(unfinished.length > unfinishedLines.length ? [`(${unfinished.length - unfinishedLines.length} older unfinished run(s) omitted from the bounded prompt ledger.)`] : []),
821
- 'When concrete work details are required, call build_history_query with history_index from this list. Do not call it merely to answer completion status already shown here.',
821
+ 'When the current task continues, fixes, verifies, or depends on earlier work in this list, proactively call build_history_query with its history_index before re-investigating. Reuse the returned tool activity and results instead of re-running commands or re-reading files this conversation already examined. Querying history is read-only and never authorizes resuming that work; do not query merely to answer completion status already shown here.',
822
822
  ].join('\n');
823
823
  }
824
824
  async function shouldStopAfterTurn(agent, message) {
@@ -219,6 +219,7 @@ export declare class ConversationKernel {
219
219
  setWorkRunExpanded(target: ConversationTargetInput, runId: string, expanded: boolean): boolean;
220
220
  setInputMode(target: ConversationTargetInput, mode: string): 'guide' | 'next';
221
221
  setMode(target: ConversationTargetInput, mode: AgentMode): AgentMode;
222
+ setModel(target: ConversationTargetInput, model: string): string;
222
223
  toggleGoalPause(target: ConversationTargetInput): Promise<boolean>;
223
224
  clearGoal(target: ConversationTargetInput): boolean;
224
225
  updateSetting(section: string, key: string, value: unknown): void;
@@ -229,6 +230,13 @@ export declare class ConversationKernel {
229
230
  prompt(message: string | AgentPromptMessage, target: ConversationTargetInput, options: ConversationKernelRunOptions, queueMode?: ConversationQueueMode): Promise<ConversationKernelRunResult>;
230
231
  private settleCooperativeStop;
231
232
  private run;
233
+ /**
234
+ * Apply a model selection recorded while a Build block was running. The
235
+ * in-flight block never switches mid-block; the switch takes effect the next
236
+ * time a queued Guide/Next re-enters the block, and only when the pending
237
+ * selection actually differs from the runner's current selection.
238
+ */
239
+ private syncPendingModel;
232
240
  private runSingle;
233
241
  private processTimeoutMs;
234
242
  private runtime;
@@ -365,6 +365,24 @@ class ConversationKernel {
365
365
  runtime.options.mode = mode;
366
366
  return runner.mode;
367
367
  }
368
+ setModel(target, model) {
369
+ const normalized = this.normalizeTarget(target);
370
+ const runtime = this.findRuntime(normalized);
371
+ const runner = runtime?.runner || this.createRunner(normalized);
372
+ if (!runtime || !runtime.activePromise) {
373
+ // No Build block is running: the selection applies immediately.
374
+ runner.setModel(model);
375
+ }
376
+ else {
377
+ // A Build block is running. The in-flight block keeps its current model
378
+ // until the next Guide/Next re-enters it; record the newly selected model
379
+ // as the pending choice so the next dequeue switches to it. This is the
380
+ // "model switch does not take effect mid-block" contract.
381
+ runtime.options.model = model;
382
+ }
383
+ runner.saveWorkspaceConversationState(true);
384
+ return runner.model;
385
+ }
368
386
  async toggleGoalPause(target) {
369
387
  const normalized = this.normalizeTarget(target);
370
388
  let runtime = this.findRuntime(normalized);
@@ -486,14 +504,19 @@ class ConversationKernel {
486
504
  }
487
505
  async prompt(message, target, options, queueMode = 'followUp') {
488
506
  const normalized = this.normalizeTarget(target);
507
+ const active = this.findRuntime(normalized);
508
+ if (active?.activePromise) {
509
+ // A Build block is already running: queue this message. Queued messages
510
+ // carry no send-time model/mode; the running block keeps its settings and
511
+ // the next dequeue follows the current conversation selection (which
512
+ // setModel/setMode already recorded on runtime.options).
513
+ this.enqueueSameSession(active, message, queueMode);
514
+ this.activateAcceptedGoal(active, typeof message === 'string' ? '' : message.goalObjective);
515
+ return active.activePromise;
516
+ }
489
517
  const runtime = this.runtime(normalized, options);
490
518
  runtime.options = { ...options };
491
519
  this.applyOptions(runtime.runner, options);
492
- if (runtime.activePromise) {
493
- this.enqueueSameSession(runtime, message, queueMode);
494
- this.activateAcceptedGoal(runtime, typeof message === 'string' ? '' : message.goalObjective);
495
- return runtime.activePromise;
496
- }
497
520
  runtime.generation = (this.generations.get(runtime.runtimeKey) || runtime.generation || 0) + 1;
498
521
  this.generations.set(runtime.runtimeKey, runtime.generation);
499
522
  const requestedRunId = typeof message === 'string' ? '' : String(message.runId || '').trim().slice(0, 200);
@@ -657,7 +680,23 @@ class ConversationKernel {
657
680
  this.mirrorHostIfTargetActive(runtime);
658
681
  return this.result(runtime, lastTokens);
659
682
  }
683
+ /**
684
+ * Apply a model selection recorded while a Build block was running. The
685
+ * in-flight block never switches mid-block; the switch takes effect the next
686
+ * time a queued Guide/Next re-enters the block, and only when the pending
687
+ * selection actually differs from the runner's current selection.
688
+ */
689
+ syncPendingModel(runtime) {
690
+ const pending = String(runtime.options.model || '').trim();
691
+ if (!pending)
692
+ return;
693
+ if (pending === runtime.runner.model || pending === runtime.runner.modelSelectionValue())
694
+ return;
695
+ runtime.runner.setModel(pending);
696
+ runtime.options.model = runtime.runner.modelSelectionValue();
697
+ }
660
698
  async runSingle(runtime, message, continuationMode) {
699
+ this.syncPendingModel(runtime);
661
700
  this.consumeQueuedMessage(runtime, typeof message === 'string' ? message : message.text);
662
701
  const timeoutMs = this.processTimeoutMs(runtime);
663
702
  if (timeoutMs <= 0) {
@@ -102,6 +102,7 @@ export declare class ElectronUtilityAgentClient {
102
102
  rateAutoRoute(score: number, routeId?: string): Promise<UtilityAutoRouteRatingResult>;
103
103
  setWorkRunExpanded(runId: string, expanded: boolean): Promise<boolean>;
104
104
  setMode(mode: AgentMode): Promise<AgentMode>;
105
+ setModel(model: string): Promise<string>;
105
106
  setInputMode(mode: string): Promise<'guide' | 'next'>;
106
107
  toggleGoalPause(): Promise<boolean>;
107
108
  clearGoal(): Promise<boolean>;
@@ -1047,6 +1047,10 @@ class ElectronUtilityAgentClient {
1047
1047
  await this.start();
1048
1048
  return await this.request('set_mode', { target: this.target, mode }, 5_000);
1049
1049
  }
1050
+ async setModel(model) {
1051
+ await this.start();
1052
+ return await this.request('set_model', { target: this.target, model }, 5_000);
1053
+ }
1050
1054
  async setInputMode(mode) {
1051
1055
  await this.start();
1052
1056
  return await this.request('set_input_mode', { target: this.target, mode }, 5_000);
@@ -18,6 +18,7 @@ export interface ElectronTargetRuntimeClient {
18
18
  rateAutoRoute?(score: number, routeId?: string): Promise<UtilityAutoRouteRatingResult>;
19
19
  setWorkRunExpanded(runId: string, expanded: boolean): Promise<boolean>;
20
20
  setMode?(mode: AgentMode): Promise<AgentMode>;
21
+ setModel?(model: string): Promise<string>;
21
22
  setInputMode?(mode: string): Promise<'guide' | 'next'>;
22
23
  toggleGoalPause?(): Promise<boolean>;
23
24
  clearGoal?(): Promise<boolean>;
@@ -79,6 +80,7 @@ export declare class ElectronUtilityRuntimePool {
79
80
  setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
80
81
  setInputMode(target: ConversationRuntimeTarget, mode: string): Promise<'guide' | 'next' | null>;
81
82
  setMode(target: ConversationRuntimeTarget, mode: AgentMode): Promise<AgentMode | null>;
83
+ setModel(target: ConversationRuntimeTarget, model: string): Promise<string | null>;
82
84
  toggleGoalPause(target: ConversationRuntimeTarget): Promise<boolean | null>;
83
85
  clearGoal(target: ConversationRuntimeTarget): Promise<boolean | null>;
84
86
  updateSetting(section: string, key: string, value: unknown): Promise<void>;
@@ -261,6 +261,17 @@ class ElectronUtilityRuntimePool {
261
261
  this.release(entry, true);
262
262
  }
263
263
  }
264
+ async setModel(target, model) {
265
+ const entry = await this.acquireExisting((0, conversationTarget_1.normalizeConversationTarget)(target));
266
+ if (!entry?.client.setModel)
267
+ return null;
268
+ try {
269
+ return await entry.client.setModel(model);
270
+ }
271
+ finally {
272
+ this.release(entry, true);
273
+ }
274
+ }
264
275
  async toggleGoalPause(target) {
265
276
  const entry = await this.acquire((0, conversationTarget_1.normalizeConversationTarget)(target));
266
277
  if (!entry?.client.toggleGoalPause)
@@ -132,6 +132,13 @@ export type UtilityAgentRequest = {
132
132
  target: ConversationRuntimeTarget;
133
133
  mode: AgentMode;
134
134
  };
135
+ } | {
136
+ id: string;
137
+ method: 'set_model';
138
+ params: {
139
+ target: ConversationRuntimeTarget;
140
+ model: string;
141
+ };
135
142
  } | {
136
143
  id: string;
137
144
  method: 'set_input_mode';
@@ -79,6 +79,7 @@ export declare class WslAgentClient {
79
79
  rateAutoRoute(target: ConversationRuntimeTarget, score: number, routeId?: string): Promise<WslAutoRouteRatingResult>;
80
80
  setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
81
81
  setMode(target: ConversationRuntimeTarget, mode: AgentMode): Promise<AgentMode>;
82
+ setModel(target: ConversationRuntimeTarget, model: string): Promise<string>;
82
83
  setInputMode(target: ConversationRuntimeTarget, mode: string): Promise<'guide' | 'next'>;
83
84
  toggleGoalPause(target: ConversationRuntimeTarget): Promise<boolean>;
84
85
  clearGoal(target: ConversationRuntimeTarget): Promise<boolean>;
@@ -337,6 +337,10 @@ class WslAgentClient {
337
337
  await this.start();
338
338
  return await this.request('set_mode', { target: await this.mapTarget(target), mode }, 5_000);
339
339
  }
340
+ async setModel(target, model) {
341
+ await this.start();
342
+ return await this.request('set_model', { target: await this.mapTarget(target), model }, 5_000);
343
+ }
340
344
  async setInputMode(target, mode) {
341
345
  await this.start();
342
346
  return await this.request('set_input_mode', { target: await this.mapTarget(target), mode }, 5_000);
@@ -144,6 +144,13 @@ export type WslAgentRequest = {
144
144
  target: ConversationRuntimeTarget;
145
145
  mode: AgentMode;
146
146
  };
147
+ } | {
148
+ id: string;
149
+ method: 'set_model';
150
+ params: {
151
+ target: ConversationRuntimeTarget;
152
+ model: string;
153
+ };
147
154
  } | {
148
155
  id: string;
149
156
  method: 'set_input_mode';
@@ -18,6 +18,7 @@ export interface WslTargetRuntimeClient {
18
18
  rateAutoRoute?(target: ConversationRuntimeTarget, score: number, routeId?: string): Promise<WslAutoRouteRatingResult>;
19
19
  setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
20
20
  setMode?(target: ConversationRuntimeTarget, mode: AgentMode): Promise<AgentMode>;
21
+ setModel?(target: ConversationRuntimeTarget, model: string): Promise<string>;
21
22
  setInputMode?(target: ConversationRuntimeTarget, mode: string): Promise<'guide' | 'next'>;
22
23
  toggleGoalPause?(target: ConversationRuntimeTarget): Promise<boolean>;
23
24
  clearGoal?(target: ConversationRuntimeTarget): Promise<boolean>;
@@ -81,6 +82,7 @@ export declare class WslAgentRuntimePool {
81
82
  setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
82
83
  setInputMode(target: ConversationRuntimeTarget, mode: string): Promise<'guide' | 'next' | null>;
83
84
  setMode(target: ConversationRuntimeTarget, mode: AgentMode): Promise<AgentMode | null>;
85
+ setModel(target: ConversationRuntimeTarget, model: string): Promise<string | null>;
84
86
  toggleGoalPause(target: ConversationRuntimeTarget): Promise<boolean | null>;
85
87
  clearGoal(target: ConversationRuntimeTarget): Promise<boolean | null>;
86
88
  updateSetting(section: string, key: string, value: unknown): Promise<void>;
@@ -276,6 +276,18 @@ class WslAgentRuntimePool {
276
276
  this.release(entry, true);
277
277
  }
278
278
  }
279
+ async setModel(target, model) {
280
+ const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
281
+ const entry = await this.acquireExisting(normalized);
282
+ if (!entry?.client.setModel)
283
+ return null;
284
+ try {
285
+ return await entry.client.setModel(normalized, model);
286
+ }
287
+ finally {
288
+ this.release(entry, true);
289
+ }
290
+ }
279
291
  async toggleGoalPause(target) {
280
292
  const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
281
293
  const entry = await this.acquire(normalized);
package/dist/main.js CHANGED
@@ -735,12 +735,21 @@ function setWindowsConsoleMode(mode) {
735
735
  const setMode = Number.isInteger(mode);
736
736
  const script = [
737
737
  'Add-Type -TypeDefinition \'using System; using System.Runtime.InteropServices; public static class NewmarkConsoleMode { [DllImport("kernel32.dll")] public static extern IntPtr GetStdHandle(int n); [DllImport("kernel32.dll")] public static extern bool GetConsoleMode(IntPtr h, out uint mode); [DllImport("kernel32.dll")] public static extern bool SetConsoleMode(IntPtr h, uint mode); }\'',
738
- '$handle = [NewmarkConsoleMode]::GetStdHandle(-10)',
739
- '$current = [uint32]0',
740
- 'if (-not [NewmarkConsoleMode]::GetConsoleMode($handle, [ref]$current)) { exit 2 }',
738
+ // 输出句柄(STD_OUTPUT_HANDLE = -11)必须启用 ENABLE_VIRTUAL_TERMINAL_PROCESSING(0x4)
739
+ // 否则 TUI 的备用屏 ?1049h / 清屏 2J 等 ANSI 序列在 ConHost/传统控制台下不被解析,
740
+ // 主屏保留滚动历史(滚轮能滚回旧帧),光标追踪也随之错位。
741
+ '$out = [NewmarkConsoleMode]::GetStdHandle(-11)',
742
+ '$outMode = [uint32]0',
743
+ 'if (-not [NewmarkConsoleMode]::GetConsoleMode($out, [ref]$outMode)) { exit 2 }',
741
744
  setMode
742
- ? `$target = [uint32]${mode}; if (-not [NewmarkConsoleMode]::SetConsoleMode($handle, $target)) { exit 3 }`
743
- : '$target = [uint32](($current -band (-bnot 7)) -bor 512); if (-not [NewmarkConsoleMode]::SetConsoleMode($handle, $target)) { exit 3 }; Write-Output $current',
745
+ ? `$target = [uint32]${mode}; if (-not [NewmarkConsoleMode]::SetConsoleMode($out, $target)) { exit 3 }`
746
+ : 'if (-not [NewmarkConsoleMode]::SetConsoleMode($out, ($outMode -bor 4))) { exit 3 }',
747
+ // 输入句柄(STD_INPUT_HANDLE = -10)启用 ENABLE_VIRTUAL_TERMINAL_INPUT(0x200),
748
+ // 清除 line/echo 让方向键等原始序列可读。
749
+ '$inp = [NewmarkConsoleMode]::GetStdHandle(-10)',
750
+ '$inpMode = [uint32]0',
751
+ 'if ([NewmarkConsoleMode]::GetConsoleMode($inp, [ref]$inpMode)) { [NewmarkConsoleMode]::SetConsoleMode($inp, (($inpMode -band (-bnot 7)) -bor 512)) | Out-Null }',
752
+ 'Write-Output $outMode',
744
753
  ].join('; ');
745
754
  const result = (0, child_process_1.spawnSync)('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script], {
746
755
  stdio: ['inherit', 'pipe', 'inherit'],
@@ -1206,6 +1215,11 @@ else if (isTuiArg) {
1206
1215
  || path.basename(process.execPath).toLowerCase() === 'newmark console runtime.exe'
1207
1216
  || process.env.NEWMARK_CONSOLE_WRAPPER === '1');
1208
1217
  if (isConsoleLauncher && process.env.NEWMARK_TUI_SIDECAR !== '1') {
1218
+ // Console mode is shared by every process attached to the same console:
1219
+ // enable output VT processing (and input VT) before spawning the sidecar so
1220
+ // the TUI's ?1049h alternate-screen sequence is actually parsed by ConHost /
1221
+ // traditional consoles. The sidecar also re-applies it defensively below.
1222
+ setWindowsConsoleMode();
1209
1223
  const tuiProcess = (0, child_process_1.spawnSync)(process.execPath, [path.join(__dirname, 'launcher.js'), ...args], {
1210
1224
  cwd: process.cwd(),
1211
1225
  env: {
@@ -3162,16 +3176,31 @@ else {
3162
3176
  });
3163
3177
  electron_1.ipcMain.handle('agent:setModel', async (_event, model) => {
3164
3178
  if (agent) {
3165
- const before = agent.model;
3179
+ // Compare the resolved selection (qualified deployment or 'auto')
3180
+ // rather than the bare model name, so switching between two
3181
+ // same-named models on different providers is still recognized as a
3182
+ // real change.
3183
+ const before = agent.modelSelectionValue();
3166
3184
  agent.setModel(model, true);
3185
+ const after = agent.modelSelectionValue();
3167
3186
  // Compression and kernel reset only make sense when the model actually
3168
3187
  // changed. The renderer sends this on every prompt, so an unchanged
3169
3188
  // selection must not trigger a context compression round (which can
3170
3189
  // run an extra model call on large histories).
3171
- if (agent.model !== before) {
3190
+ if (after !== before) {
3172
3191
  await agent.compressForModelSwitch();
3173
3192
  resetConversationKernel();
3174
3193
  }
3194
+ // Propagate the selection to the target-bound runtime. A running Build
3195
+ // block keeps its current model until the next Guide/Next re-enters it;
3196
+ // the runtime records the new selection as pending so the context
3197
+ // window and the next dequeue both follow the newly selected model.
3198
+ const target = conversationRuntimeTarget(agent.activeConversationId || 'default');
3199
+ ensureConversationKernel(root)?.setModel(target, model);
3200
+ if (wslBackendEnabled())
3201
+ await ensureWslConversationPool()?.setModel(target, model);
3202
+ else
3203
+ await ensureElectronUtilityPool()?.setModel(target, model);
3175
3204
  }
3176
3205
  return agent?.model;
3177
3206
  });
@@ -350,7 +350,7 @@ class ToolExecutor {
350
350
  t('subagent_result', 'Return the persisted transcript, mailbox summary, status, and latest result for a peer agent. Target by exact id (preferred) or name.', { id: { type: 'string', description: 'Exact peer id from subagent_list.' }, name: { type: 'string', description: 'Convenience peer name.' } }, []),
351
351
  t('subagent_close', 'Close a same-conversation peer. Root can close any peer; a peer can close only itself. Target by exact id (preferred) or name.', { id: { type: 'string', description: 'Exact peer id from subagent_list.' }, name: { type: 'string', description: 'Convenience peer name.' } }, []),
352
352
  t('linked_plan', 'Read or update the current conversation linked Markdown plan. Update requires the current expected_revision.', { action: { type: 'string', enum: ['get', 'update'] }, markdown: { type: 'string' }, expected_revision: { type: 'number' } }, ['action']),
353
- t('build_history_query', 'Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query. Every activity/guide content is bounded to max_chars (default 2000) to keep the read lean and cache-friendly.', { history_index: { type: 'number', minimum: 1, description: '1-based historical Build Block index from the request ledger; 1 is the newest previous task.' }, run_id: { type: 'string', description: 'Exact run id returned by an earlier build_history_query result.' }, max_events: { type: 'number', minimum: 1, maximum: 200, description: 'Maximum trailing public work events; defaults to 80.' }, max_chars: { type: 'number', minimum: 100, maximum: 4000, description: 'Per-event/per-guide content character bound; defaults to 2000.' } }, []),
353
+ t('build_history_query', 'Read the concrete public work details (tool calls, results, file changes, guides) of one historical Build Block. Call it proactively when the current task continues, fixes, verifies, or depends on earlier work: reuse the returned activity instead of re-investigating from scratch. Do not call it merely to answer completion status already exposed by the prompt. Select by newest-to-oldest history_index, or by run_id returned from an earlier query. Every activity/guide content is bounded to max_chars (default 2000) to keep the read lean and cache-friendly.', { history_index: { type: 'number', minimum: 1, description: '1-based historical Build Block index from the request ledger; 1 is the newest previous task.' }, run_id: { type: 'string', description: 'Exact run id returned by an earlier build_history_query result.' }, max_events: { type: 'number', minimum: 1, maximum: 200, description: 'Maximum trailing public work events; defaults to 80.' }, max_chars: { type: 'number', minimum: 100, maximum: 4000, description: 'Per-event/per-guide content character bound; defaults to 2000.' } }, []),
354
354
  t('context_compress', 'Actively compress the LLM context history for this conversation. This collapses older history entries into a concise summary while preserving the recent tail, which reduces context tokens and cost. IMPORTANT: it affects only the LLM context (what the model sees); the displayed conversation history shown to the user is never altered. Call this when the conversation is long, token pressure is high, or you judge that older turns are no longer needed in full. Idempotent and safe: repeated calls produce incremental summaries.', { keep_recent: { type: 'number', minimum: 2, maximum: 60, description: 'Recent message count to keep uncompressed at the tail. Defaults to the configured keep_recent_messages.' }, force: { type: 'boolean', description: 'Compress even if the context is not yet over the automatic threshold. Defaults to false.' } }, []),
355
355
  t('context_history_manage', 'Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. The hot cache stays bounded; evicted folded segments remain in a conversation-isolated append-only cold archive and are loaded only by explicit search/read/restore calls. Actions: list returns a bounded index of current context entries; remove declares one long-term entry for unload (see below); summarize folds a contiguous current range; restore reinserts a folded segment when its summary marker is still present; search finds matching hot or archived segments; read returns one bounded segment without injecting the whole archive; status reports budgets, hot cache, cold archive, the protected recent zone, and pending removals. The recent context tail and last user message are protected from remove/summarize unless dangerous is true. For cache-optimization, remove ONLY targets long-term history (never the protected recent tail or last user message) and does NOT unload immediately: the declared entry stays in context for the rest of the current Build Block so the provider prefix cache stays stable, then is physically removed when the Block ends — applying to subsequent Blocks only.', {
356
356
  action: { type: 'string', enum: ['list', 'remove', 'summarize', 'restore', 'search', 'read', 'status'], description: 'list current entries; remove one; summarize a range; restore by restore_id; search hot/cold folded segments; read one bounded folded segment; status report context budgets and storage.' },
@@ -51,8 +51,10 @@ const ESC = "\u001b[";
51
51
  function createPaintScheduler(state, output = process.stdout, renderFrame = render) {
52
52
  let pending = false;
53
53
  let lastFrame = "";
54
+ let cancelled = false;
54
55
  const flush = () => {
55
56
  pending = false;
57
+ if (cancelled) return;
56
58
  const frame = renderFrame(state);
57
59
  if (frame === lastFrame) return false;
58
60
  lastFrame = frame;
@@ -65,6 +67,8 @@ function createPaintScheduler(state, output = process.stdout, renderFrame = rend
65
67
  setImmediate(flush);
66
68
  };
67
69
  paint.flush = flush;
70
+ // 退出前调用:丢弃任何排队中的重绘,避免恢复主屏后又被画上一帧 TUI 画面。
71
+ paint.cancel = () => { cancelled = true; pending = false; };
68
72
  return paint;
69
73
  }
70
74
 
@@ -108,6 +112,28 @@ function resolveTuiWorkspacePath(args, options = {}) {
108
112
  return explicitRoot || process.cwd();
109
113
  }
110
114
 
115
+ // 在 Windows 传统控制台(ConHost)下,输出句柄默认不启用
116
+ // ENABLE_VIRTUAL_TERMINAL_PROCESSING,导致备用屏 ?1049h / 清屏 2J 等 ANSI
117
+ // 序列不被解析,主屏保留滚动历史(滚轮能滚回旧帧)。Electron-as-node sidecar
118
+ // 也不会像纯 Node 那样由 libuv 自动启用 VT。这里主动给输出句柄启用 VT 处理,
119
+ // 给输入句柄启用原始输入,作为 main.ts 之外入口的保险。
120
+ function enableWindowsVirtualTerminal() {
121
+ if (process.platform !== 'win32') return;
122
+ try {
123
+ const { spawnSync } = require('node:child_process');
124
+ const script = [
125
+ 'Add-Type -TypeDefinition \'using System; using System.Runtime.InteropServices; public static class NewmarkTuiVT { [DllImport("kernel32.dll")] public static extern IntPtr GetStdHandle(int n); [DllImport("kernel32.dll")] public static extern bool GetConsoleMode(IntPtr h, out uint m); [DllImport("kernel32.dll")] public static extern bool SetConsoleMode(IntPtr h, uint m); }\'',
126
+ '$o = [NewmarkTuiVT]::GetStdHandle(-11); $om = [uint32]0; if ([NewmarkTuiVT]::GetConsoleMode($o, [ref]$om)) { [NewmarkTuiVT]::SetConsoleMode($o, ($om -bor 4)) | Out-Null }',
127
+ '$i = [NewmarkTuiVT]::GetStdHandle(-10); $im = [uint32]0; if ([NewmarkTuiVT]::GetConsoleMode($i, [ref]$im)) { [NewmarkTuiVT]::SetConsoleMode($i, (($im -band (-bnot 7)) -bor 512)) | Out-Null }',
128
+ ].join('; ');
129
+ spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script], {
130
+ stdio: 'ignore',
131
+ windowsHide: true,
132
+ timeout: 3000,
133
+ });
134
+ } catch {}
135
+ }
136
+
111
137
  function start(options = {}) {
112
138
  const forcedTerminal = process.env.NEWMARK_FORCE_TTY === "1";
113
139
  if ((!process.stdin.isTTY || !process.stdout.isTTY) && !forcedTerminal) {
@@ -115,6 +141,9 @@ function start(options = {}) {
115
141
  process.exitCode = 1;
116
142
  return;
117
143
  }
144
+ // 备用屏依赖输出句柄的 VT 处理;纯 Node 的 libuv 会自动启用,但
145
+ // Electron-as-node sidecar 或强制 TTY 路径不会,这里补上。
146
+ if (forcedTerminal || !process.stdout.isTTY) enableWindowsVirtualTerminal();
118
147
 
119
148
  const args = process.argv.slice(2);
120
149
  let adapter;
@@ -140,24 +169,38 @@ function start(options = {}) {
140
169
  let timer = null;
141
170
  let animationTimer = null;
142
171
  let closing = false;
172
+ // 标准 TUI 屏幕模型:进入备用屏幕缓冲(alternate screen buffer)。
173
+ // 备用屏没有主屏的滚动历史:每次刷新用 2J 全量重绘而不是滚动新页面,
174
+ // 鼠标滚轮无法滚回上一次刷新,终端滚动也不会让画面错位(否则高亮行
175
+ // 会随终端滚动"离开屏幕")。
176
+ const enterAltScreen = () => process.stdout.write(`${ESC}?1049h${ESC}?25l${ESC}2J${ESC}H`);
177
+ const leaveAltScreen = () => `${ESC}?25h${ESC}?1049l${ESC}0m${ESC}2J${ESC}H`;
143
178
  const paint = createPaintScheduler(state);
144
179
  const cleanup = () => {
145
180
  if (timer) clearInterval(timer);
146
181
  if (animationTimer) clearInterval(animationTimer);
147
182
  if (typeof process.stdin.setRawMode === "function") process.stdin.setRawMode(false);
148
183
  process.stdin.pause();
149
- process.stdout.write(`${ESC}?25h${ESC}0m${ESC}2J${ESC}H`);
150
184
  };
151
185
  const quit = () => {
152
186
  if (closing) return;
153
187
  closing = true;
188
+ paint.cancel();
154
189
  cleanup();
155
190
  if (typeof state.adapter.close === "function") state.adapter.close();
156
- process.stdout.write(state.adapterKind === "mock"
157
- ? "Newmark TUI demo closed. No state was saved.\n"
158
- : "Newmark TUI closed. Conversation and settings state are persisted by Newmark.\n");
191
+ // 退出序列与关闭消息用 fs.writeSync 同步写入 fd:直接进入内核管道,
192
+ // 不经过异步流缓冲,process.exit() 不会丢弃任何字节(此前异步 write
193
+ // 在退出瞬间被 ConPTY/管道丢弃,导致恢复主屏后看不到退出消息)。
194
+ try {
195
+ const fs = require("node:fs");
196
+ const fd = process.stdout.fd || 1;
197
+ fs.writeSync(fd, `${leaveAltScreen()}\n`);
198
+ fs.writeSync(fd, state.adapterKind === "mock"
199
+ ? "Newmark TUI demo closed. No state was saved.\n"
200
+ : "Newmark TUI closed. Conversation and settings state are persisted by Newmark.\n");
201
+ } catch {}
159
202
  process.exitCode = 0;
160
- setImmediate(() => process.exit(0));
203
+ process.exit(0);
161
204
  };
162
205
 
163
206
  function simulateReply(text) {
@@ -554,8 +597,10 @@ function start(options = {}) {
554
597
  process.stdin.resume();
555
598
  process.stdin.on("keypress", handleKey);
556
599
  process.stdout.on("resize", paint);
557
- process.on("exit", () => process.stdout.write(`${ESC}?25h${ESC}0m`));
600
+ // 兜底:任何退出路径(包括未捕获异常后的 exit 事件)都恢复主屏缓冲与光标。
601
+ process.on("exit", () => process.stdout.write(`${ESC}?25h${ESC}?1049l${ESC}0m`));
558
602
  process.on("SIGTERM", quit);
603
+ enterAltScreen();
559
604
  paint();
560
605
  }
561
606
 
@@ -204,6 +204,34 @@ const providers = [
204
204
  }
205
205
  ];
206
206
 
207
+ // Demo-only long-list injection: NEWMARK_TUI_DEMO_MODELS=<count> appends a
208
+ // dedicated stress provider with <count> models so the featureless --demo TUI
209
+ // can exercise long model-selection cursor-follow behavior without any runtime.
210
+ const DEMO_MODEL_COUNT = Number(process.env.NEWMARK_TUI_DEMO_MODELS || 0);
211
+ if (Number.isFinite(DEMO_MODEL_COUNT) && DEMO_MODEL_COUNT > 0) {
212
+ providers.push({
213
+ id: "provider-demo-stress",
214
+ name: "Demo Stress",
215
+ base_url: "https://demo.invalid/v1",
216
+ api_key: "",
217
+ has_api_key: false,
218
+ protocol: "openai",
219
+ enabled: true,
220
+ models: Array.from({ length: DEMO_MODEL_COUNT }, (_, index) => ({
221
+ name: `demo-stress-model-${String(index).padStart(3, "0")}`,
222
+ display: `Stress Model ${String(index).padStart(3, "0")}`,
223
+ description: `Long-list cursor-follow stress model ${index} description`,
224
+ max_tokens: 128000,
225
+ vision: false,
226
+ thinking: false,
227
+ enabled: true,
228
+ speed_rating: "unknown",
229
+ capability_rating: "unknown",
230
+ validation: { status: "unavailable", level: "discovered", checked_at: "" }
231
+ }))
232
+ });
233
+ }
234
+
207
235
  const flows = [
208
236
  {
209
237
  name: "release-readiness",
@@ -689,29 +689,34 @@ function modelView(state, width, p) {
689
689
  const isCurrent = (selection) => selection.kind === current.kind
690
690
  && (selection.kind === "auto"
691
691
  || (selection.providerId === current.providerId && selection.modelId === current.modelId));
692
- return [
692
+ const rows = [
693
693
  ...conversationContext(state, p, "Model and reasoning effort"),
694
- `${p.bold}${tr(state, "Reasoning effort")}${p.reset} ${p.muted}${tr(state, "Shared GUI/TUI request tier · ←/→ changes section")}${p.reset}`,
695
- ...INTELLIGENCE_TIERS.map((tier, index) => {
696
- const style = state.focusRegion === "content" && state.contentColumn === 0 && index === state.selected ? `${p.selected}${p.bold}` : "";
697
- if (style) state.contentFocusLine = 5 + index;
698
- return `${style} ${tier === currentTier ? `${p.cyan}●${p.reset}` : ""} ${tier}${p.reset}`;
699
- }),
694
+ `${p.bold}${tr(state, "Reasoning effort")}${p.reset} ${p.muted}${tr(state, "Shared GUI/TUI request tier · ←/→ changes section")}${p.reset}`
695
+ ];
696
+ const tierStart = rows.length;
697
+ INTELLIGENCE_TIERS.forEach((tier, index) => {
698
+ const style = state.focusRegion === "content" && state.contentColumn === 0 && index === state.selected ? `${p.selected}${p.bold}` : "";
699
+ if (style) state.contentFocusLine = tierStart + index;
700
+ rows.push(`${style} ${tier === currentTier ? `${p.cyan}●${p.reset}` : "○"} ${tier}${p.reset}`);
701
+ });
702
+ rows.push(
700
703
  "",
701
704
  `${p.bold}${tr(state, "Deployment")}${p.reset} ${p.muted}${tr(state, "Used by this conversation, including its Plan and Subagents")}${p.reset}`,
702
- "",
703
- ...options.flatMap((option, index) => {
704
- const style = state.focusRegion === "content" && state.contentColumn === 1 && index === state.selected ? `${p.selected}${p.bold}` : "";
705
- if (style) state.contentFocusLine = 14 + index * 2;
706
- const marker = isCurrent(option.selection) ? `${p.cyan}●${p.reset}` : "";
707
- return [
708
- `${style} ${marker} ${pad(option.label, Math.max(18, Math.min(32, width - 24)))} ${p.muted}${option.provider}${p.reset}`,
709
- ` ${p.muted}${truncate(option.description, Math.max(20, width - 5))}${p.reset}`
710
- ];
711
- }),
705
+ ""
706
+ );
707
+ const deploymentStart = rows.length;
708
+ options.forEach((option, index) => {
709
+ const style = state.focusRegion === "content" && state.contentColumn === 1 && index === state.selected ? `${p.selected}${p.bold}` : "";
710
+ if (style) state.contentFocusLine = deploymentStart + index * 2;
711
+ const marker = isCurrent(option.selection) ? `${p.cyan}●${p.reset}` : "○";
712
+ rows.push(`${style} ${marker} ${pad(option.label, Math.max(18, Math.min(32, width - 24)))} ${p.muted}${option.provider}${p.reset}`);
713
+ rows.push(` ${p.muted}${truncate(option.description, Math.max(20, width - 5))}${p.reset}`);
714
+ });
715
+ rows.push(
712
716
  "",
713
717
  `${p.muted}${tr(state, "Enter applies the focused tier or deployment. Effort persists globally; deployments remain per conversation.")}${p.reset}`
714
- ];
718
+ );
719
+ return rows;
715
720
  }
716
721
 
717
722
  function flowBarView(state, width, p) {
@@ -1250,14 +1255,38 @@ function overlayLines(state, width, p) {
1250
1255
  return [];
1251
1256
  }
1252
1257
 
1253
- function render(state, columns = process.stdout.columns || 100, rows = process.stdout.rows || 30) {
1258
+ // 实时读取终端窗口尺寸:优先 getWindowSize()(每次调用都查询底层 TTY 尺寸,
1259
+ // 不依赖 Node 缓存的 columns/rows,避免 resize 事件丢失或延迟时按旧尺寸绘制),
1260
+ // 其次回退到缓存 columns/rows,再回退 COLUMNS/LINES 环境变量与默认值。
1261
+ function readWindowSize(fallbackColumns = 100, fallbackRows = 30) {
1262
+ const stdout = process.stdout;
1263
+ if (stdout && typeof stdout.getWindowSize === "function") {
1264
+ try {
1265
+ const [width, height] = stdout.getWindowSize();
1266
+ if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
1267
+ return { columns: Math.max(1, Math.floor(width)), rows: Math.max(1, Math.floor(height)) };
1268
+ }
1269
+ } catch {}
1270
+ }
1271
+ const columns = Number(stdout?.columns) || Number(process.env.COLUMNS) || fallbackColumns;
1272
+ const rows = Number(stdout?.rows) || Number(process.env.LINES) || fallbackRows;
1273
+ return { columns: Math.max(1, Math.floor(columns)), rows: Math.max(1, Math.floor(rows)) };
1274
+ }
1275
+
1276
+ function render(state, columns, rows) {
1277
+ const size = (columns !== undefined && rows !== undefined)
1278
+ ? { columns: Math.max(1, Math.floor(Number(columns) || 1)), rows: Math.max(1, Math.floor(Number(rows) || 1)) }
1279
+ : readWindowSize();
1254
1280
  const p = palette(state);
1255
- const width = Math.max(52, columns);
1256
- const height = Math.max(20, rows);
1281
+ // 严格约束:直接采用终端实际尺寸,绝不通过 Math.max(52/20) 强制放大。
1282
+ // 终端窗口小于内部最小布局时,内容在输出阶段裁剪到窗口内,否则帧会
1283
+ // 画到窗口之外(用户看不到底部行/右侧列,如小窗口下的模型长菜单)。
1284
+ const width = size.columns;
1285
+ const height = size.rows;
1257
1286
  const compact = width < 78;
1258
1287
  const sidebarWidth = compact ? 0 : 22;
1259
1288
  const contentWidth = width - sidebarWidth - 2;
1260
- const bodyHeight = height - 3;
1289
+ const bodyHeight = Math.max(1, height - 3);
1261
1290
  const sidebar = sidebarWidth ? renderSidebar(state, bodyHeight, sidebarWidth, p) : [];
1262
1291
  const content = renderContent(state, contentWidth, bodyHeight, p);
1263
1292
  let contentLines = content;
@@ -1266,8 +1295,11 @@ function render(state, columns = process.stdout.columns || 100, rows = process.s
1266
1295
  let scroll = Math.max(0, Math.min(maximumScroll, Number(state.contentScroll) || 0));
1267
1296
  const focusLine = Number(state.contentFocusLine) || -1;
1268
1297
  if (focusLine >= 0) {
1269
- if (focusLine < scroll) scroll = focusLine;
1270
- else if (focusLine >= scroll + bodyHeight) scroll = focusLine - bodyHeight + 1;
1298
+ // 居中跟随:焦点行尽量保持在视口中上部,上下都留出内容,避免选中行
1299
+ // 贴住视口边缘或被裁掉其相邻行(如两行一组的模型选项主行+描述行)。
1300
+ const targetLine = Math.min(Math.max(0, Math.floor(bodyHeight / 3)), Math.max(0, bodyHeight - 2));
1301
+ if (focusLine < scroll + targetLine) scroll = Math.max(0, focusLine - targetLine);
1302
+ else if (focusLine >= scroll + bodyHeight - 1 - targetLine) scroll = Math.min(maximumScroll, focusLine - (bodyHeight - 1 - targetLine));
1271
1303
  }
1272
1304
  scroll = Math.max(0, Math.min(maximumScroll, scroll));
1273
1305
  state.contentScroll = scroll;
@@ -1286,20 +1318,25 @@ function render(state, columns = process.stdout.columns || 100, rows = process.s
1286
1318
  : "";
1287
1319
  if (compact) body.unshift(pad(top, width));
1288
1320
  const focus = state.focusRegion === "menu" ? "MENU" : "CONTENT";
1289
- const footer = `${p.panel} ${p.cyan}${focus}${p.reset}${p.panel}${p.muted} · ${truncate(state.notice, width - 42)}${p.reset}${p.panel}${" ".repeat(Math.max(1, width - visibleLength(state.notice) - visibleLength(focus) - 35))}Tab back ? help Q quit ${p.reset}`;
1290
- const renderedBody = body.slice(0, height - 1);
1291
- while (renderedBody.length < height - 1) renderedBody.push(pad("", width));
1292
- let output = `${ESC}?25l${ESC}2J${ESC}H${p.paint}${renderedBody.join("\n")}\n${pad(footer, width)}`;
1321
+ const footer = `${p.panel} ${p.cyan}${focus}${p.reset}${p.panel}${p.muted} · ${truncate(state.notice, Math.max(1, width - 42))}${p.reset}${p.panel}${" ".repeat(Math.max(1, width - visibleLength(state.notice) - visibleLength(focus) - 35))}Tab back ? help Q quit ${p.reset}`;
1322
+ const bodyLines = Math.max(1, height - 1);
1323
+ const renderedBody = body.slice(0, bodyLines);
1324
+ while (renderedBody.length < bodyLines) renderedBody.push(pad("", width));
1325
+ // 输出阶段严格边界:每行按可见宽度截断到窗口宽度,行数限制在窗口高度内,
1326
+ // 确保无论内部最小布局如何,帧绝不会画到窗口之外。
1327
+ const boundedBody = renderedBody.map((line) => truncate(line, width));
1328
+ const boundedFooter = truncate(pad(footer, width), width);
1329
+ let output = `${ESC}?25l${ESC}2J${ESC}H${p.paint}${boundedBody.join("\n")}\n${boundedFooter}`;
1293
1330
  const overlay = overlayLines(state, width, p);
1294
1331
  if (overlay.length) {
1295
1332
  const overlayWidth = Math.max(...overlay.map(visibleLength));
1296
1333
  const x = Math.max(1, Math.floor((width - overlayWidth) / 2) + 1);
1297
1334
  const y = Math.max(2, Math.floor((height - overlay.length) / 2));
1298
1335
  overlay.forEach((line, index) => {
1299
- output += `${ESC}${y + index};${x}H${line}`;
1336
+ output += `${ESC}${y + index};${x}H${truncate(line, width)}`;
1300
1337
  });
1301
1338
  }
1302
1339
  return `${output}${p.final}`;
1303
1340
  }
1304
1341
 
1305
- module.exports = { render, stripAnsi, visibleLength, wrapText };
1342
+ module.exports = { render, readWindowSize, stripAnsi, visibleLength, wrapText };
@@ -22825,6 +22825,17 @@ function schedulePostStartupUiRendering() {
22825
22825
  var modelResult = await api.setModel(state.model);
22826
22826
  if (modelResult != null) state._syncedModel = state.model;
22827
22827
  }
22828
+ // The context ring/inspector must immediately reflect the newly selected
22829
+ // model's configured context window rather than waiting for the next send.
22830
+ if (api.getState) {
22831
+ var ctxTarget = currentConversationTarget();
22832
+ api.getState(ctxTarget).then(function(s) {
22833
+ if (s && s.contextWindow !== undefined && isActiveConversationTarget(ctxTarget) && isViewingRuntimeConversationBranch(ctxTarget)) {
22834
+ state.contextWindow = s.contextWindow;
22835
+ window.renderContextWindow();
22836
+ }
22837
+ }).catch(function(){});
22838
+ }
22828
22839
  });
22829
22840
  }
22830
22841
 
@@ -336409,7 +336409,7 @@ var ToolExecutor = class {
336409
336409
  t3("subagent_result", "Return the persisted transcript, mailbox summary, status, and latest result for a peer agent. Target by exact id (preferred) or name.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name." } }, []),
336410
336410
  t3("subagent_close", "Close a same-conversation peer. Root can close any peer; a peer can close only itself. Target by exact id (preferred) or name.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name." } }, []),
336411
336411
  t3("linked_plan", "Read or update the current conversation linked Markdown plan. Update requires the current expected_revision.", { action: { type: "string", enum: ["get", "update"] }, markdown: { type: "string" }, expected_revision: { type: "number" } }, ["action"]),
336412
- t3("build_history_query", "Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query. Every activity/guide content is bounded to max_chars (default 2000) to keep the read lean and cache-friendly.", { history_index: { type: "number", minimum: 1, description: "1-based historical Build Block index from the request ledger; 1 is the newest previous task." }, run_id: { type: "string", description: "Exact run id returned by an earlier build_history_query result." }, max_events: { type: "number", minimum: 1, maximum: 200, description: "Maximum trailing public work events; defaults to 80." }, max_chars: { type: "number", minimum: 100, maximum: 4e3, description: "Per-event/per-guide content character bound; defaults to 2000." } }, []),
336412
+ t3("build_history_query", "Read the concrete public work details (tool calls, results, file changes, guides) of one historical Build Block. Call it proactively when the current task continues, fixes, verifies, or depends on earlier work: reuse the returned activity instead of re-investigating from scratch. Do not call it merely to answer completion status already exposed by the prompt. Select by newest-to-oldest history_index, or by run_id returned from an earlier query. Every activity/guide content is bounded to max_chars (default 2000) to keep the read lean and cache-friendly.", { history_index: { type: "number", minimum: 1, description: "1-based historical Build Block index from the request ledger; 1 is the newest previous task." }, run_id: { type: "string", description: "Exact run id returned by an earlier build_history_query result." }, max_events: { type: "number", minimum: 1, maximum: 200, description: "Maximum trailing public work events; defaults to 80." }, max_chars: { type: "number", minimum: 100, maximum: 4e3, description: "Per-event/per-guide content character bound; defaults to 2000." } }, []),
336413
336413
  t3("context_compress", "Actively compress the LLM context history for this conversation. This collapses older history entries into a concise summary while preserving the recent tail, which reduces context tokens and cost. IMPORTANT: it affects only the LLM context (what the model sees); the displayed conversation history shown to the user is never altered. Call this when the conversation is long, token pressure is high, or you judge that older turns are no longer needed in full. Idempotent and safe: repeated calls produce incremental summaries.", { keep_recent: { type: "number", minimum: 2, maximum: 60, description: "Recent message count to keep uncompressed at the tail. Defaults to the configured keep_recent_messages." }, force: { type: "boolean", description: "Compress even if the context is not yet over the automatic threshold. Defaults to false." } }, []),
336414
336414
  t3("context_history_manage", "Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. The hot cache stays bounded; evicted folded segments remain in a conversation-isolated append-only cold archive and are loaded only by explicit search/read/restore calls. Actions: list returns a bounded index of current context entries; remove declares one long-term entry for unload (see below); summarize folds a contiguous current range; restore reinserts a folded segment when its summary marker is still present; search finds matching hot or archived segments; read returns one bounded segment without injecting the whole archive; status reports budgets, hot cache, cold archive, the protected recent zone, and pending removals. The recent context tail and last user message are protected from remove/summarize unless dangerous is true. For cache-optimization, remove ONLY targets long-term history (never the protected recent tail or last user message) and does NOT unload immediately: the declared entry stays in context for the rest of the current Build Block so the provider prefix cache stays stable, then is physically removed when the Block ends \u2014 applying to subsequent Blocks only.", {
336415
336415
  action: { type: "string", enum: ["list", "remove", "summarize", "restore", "search", "read", "status"], description: "list current entries; remove one; summarize a range; restore by restore_id; search hot/cold folded segments; read one bounded folded segment; status report context budgets and storage." },
@@ -340904,7 +340904,7 @@ function buildConversationTaskLedger(agent) {
340904
340904
  "Unfinished Continuation Queue (newest to oldest; summary fields only; use only when the current user instruction authorizes continuation and the task is relevant):",
340905
340905
  ...unfinishedLines.length ? unfinishedLines : ["(none)"],
340906
340906
  ...unfinished.length > unfinishedLines.length ? [`(${unfinished.length - unfinishedLines.length} older unfinished run(s) omitted from the bounded prompt ledger.)`] : [],
340907
- "When concrete work details are required, call build_history_query with history_index from this list. Do not call it merely to answer completion status already shown here."
340907
+ "When the current task continues, fixes, verifies, or depends on earlier work in this list, proactively call build_history_query with its history_index before re-investigating. Reuse the returned tool activity and results instead of re-running commands or re-reading files this conversation already examined. Querying history is read-only and never authorizes resuming that work; do not query merely to answer completion status already shown here."
340908
340908
  ].join("\n");
340909
340909
  }
340910
340910
  async function shouldStopAfterTurn(agent, message) {
@@ -349172,8 +349172,13 @@ ${summary}`, segment, "local-summarize", true);
349172
349172
  };
349173
349173
  }
349174
349174
  resolveWindowModel(modelName) {
349175
- if (modelName !== "auto") return this.config.findModel(modelName);
349176
- return this.activeModelConfig() || this.config.findModel(this.config.getStr("models", "default_model"));
349175
+ if (modelName === "auto" || modelName === this.model || modelName === this.activeModelName()) {
349176
+ const active = this.activeModelConfig();
349177
+ if (active) return active;
349178
+ }
349179
+ const byName = this.config.findModel(modelName);
349180
+ if (byName) return byName;
349181
+ return this.config.findModel(this.config.getStr("models", "default_model"));
349177
349182
  }
349178
349183
  contextMaxTokens(modelName = this.model) {
349179
349184
  const model = this.resolveWindowModel(modelName);
@@ -352337,7 +352342,7 @@ ${custom}`);
352337
352342
  "- Memory Lab is governed by an explicit Policy chain: pre-think whether memory is needed; prefer bounded memory_lab_query retrieval; then choose ADD/UPDATE/DELETE only when the user authorizes durable memory mutation.",
352338
352343
  "- Before memory_lab_update, inspect the target with memory_lab_query or memory_lab_read. For an existing component pass expectedUpdatedAt so concurrent/stale writes fail closed; preserve established tag parent paths.",
352339
352344
  "- Use memory_lab_delete only for an explicit user request to forget/remove memory. Prior revisions are retained under Memory Lab/archive and mutation decisions are appended to policy.jsonl for replay.",
352340
- "- Build history disclosure is two-layered. The request prompt contains only each historical Build Block user input, final summary, and completion status. Use build_history_query only when the current user asks what specifically happened in one Build Block; querying history is read-only and never authorizes resuming that work.",
352345
+ "- Build history disclosure is two-layered. The request prompt contains only each historical Build Block user input, final summary, and completion status. When the current task continues, fixes, verifies, or depends on earlier Build Blocks, proactively call build_history_query to read the concrete tool activity and results of the relevant block, and reuse that information instead of re-investigating (re-running commands or re-reading files) from scratch. Querying history is read-only and never authorizes resuming that work; do not query merely to answer completion status already shown in the prompt.",
352341
352346
  "- Linked plan disclosure: a durable conversation-linked Markdown plan exists and can be inspected or updated with linked_plan when explicitly needed or required by Plan mode. Its full Markdown and revision are not injected into every model request.",
352342
352347
  "- A memory_lab_update, memory_lab_delete, or memory_lab_reindex call is unfinished until its awaited tool result contains rebuildReceipt.completed=true. The completion receipt is represented by the tool activity inside the current Build block and should not be repeated as a separate completion message.",
352343
352348
  `- Skills and subagents: skill searches enabled metadata and loads one SKILL.md body on demand; skill_download installs offline skill folders; task creates constrained subagents tracked in agent state.`,
@@ -353135,6 +353140,18 @@ var ConversationKernel = class {
353135
353140
  if (runtime) runtime.options.mode = mode;
353136
353141
  return runner.mode;
353137
353142
  }
353143
+ setModel(target, model) {
353144
+ const normalized = this.normalizeTarget(target);
353145
+ const runtime = this.findRuntime(normalized);
353146
+ const runner = runtime?.runner || this.createRunner(normalized);
353147
+ if (!runtime || !runtime.activePromise) {
353148
+ runner.setModel(model);
353149
+ } else {
353150
+ runtime.options.model = model;
353151
+ }
353152
+ runner.saveWorkspaceConversationState(true);
353153
+ return runner.model;
353154
+ }
353138
353155
  async toggleGoalPause(target) {
353139
353156
  const normalized = this.normalizeTarget(target);
353140
353157
  let runtime = this.findRuntime(normalized);
@@ -353249,14 +353266,15 @@ var ConversationKernel = class {
353249
353266
  }
353250
353267
  async prompt(message, target, options, queueMode = "followUp") {
353251
353268
  const normalized = this.normalizeTarget(target);
353269
+ const active = this.findRuntime(normalized);
353270
+ if (active?.activePromise) {
353271
+ this.enqueueSameSession(active, message, queueMode);
353272
+ this.activateAcceptedGoal(active, typeof message === "string" ? "" : message.goalObjective);
353273
+ return active.activePromise;
353274
+ }
353252
353275
  const runtime = this.runtime(normalized, options);
353253
353276
  runtime.options = { ...options };
353254
353277
  this.applyOptions(runtime.runner, options);
353255
- if (runtime.activePromise) {
353256
- this.enqueueSameSession(runtime, message, queueMode);
353257
- this.activateAcceptedGoal(runtime, typeof message === "string" ? "" : message.goalObjective);
353258
- return runtime.activePromise;
353259
- }
353260
353278
  runtime.generation = (this.generations.get(runtime.runtimeKey) || runtime.generation || 0) + 1;
353261
353279
  this.generations.set(runtime.runtimeKey, runtime.generation);
353262
353280
  const requestedRunId = typeof message === "string" ? "" : String(message.runId || "").trim().slice(0, 200);
@@ -353401,7 +353419,21 @@ Review this persisted peer result and summarize or continue the parent task as n
353401
353419
  this.mirrorHostIfTargetActive(runtime);
353402
353420
  return this.result(runtime, lastTokens);
353403
353421
  }
353422
+ /**
353423
+ * Apply a model selection recorded while a Build block was running. The
353424
+ * in-flight block never switches mid-block; the switch takes effect the next
353425
+ * time a queued Guide/Next re-enters the block, and only when the pending
353426
+ * selection actually differs from the runner's current selection.
353427
+ */
353428
+ syncPendingModel(runtime) {
353429
+ const pending3 = String(runtime.options.model || "").trim();
353430
+ if (!pending3) return;
353431
+ if (pending3 === runtime.runner.model || pending3 === runtime.runner.modelSelectionValue()) return;
353432
+ runtime.runner.setModel(pending3);
353433
+ runtime.options.model = runtime.runner.modelSelectionValue();
353434
+ }
353404
353435
  async runSingle(runtime, message, continuationMode) {
353436
+ this.syncPendingModel(runtime);
353405
353437
  this.consumeQueuedMessage(runtime, typeof message === "string" ? message : message.text);
353406
353438
  const timeoutMs = this.processTimeoutMs(runtime);
353407
353439
  if (timeoutMs <= 0) {
@@ -354100,6 +354132,9 @@ async function handle(request) {
354100
354132
  if (request.method === "set_mode") {
354101
354133
  return kernel.setMode(requestTarget(request.params), request.params.mode);
354102
354134
  }
354135
+ if (request.method === "set_model") {
354136
+ return kernel.setModel(requestTarget(request.params), request.params.model);
354137
+ }
354103
354138
  if (request.method === "set_input_mode") {
354104
354139
  return kernel.setInputMode(requestTarget(request.params), request.params.mode);
354105
354140
  }
@@ -206,6 +206,9 @@ async function handle(request) {
206
206
  if (request.method === 'set_mode') {
207
207
  return kernel.setMode(requestTarget(request.params), request.params.mode);
208
208
  }
209
+ if (request.method === 'set_model') {
210
+ return kernel.setModel(requestTarget(request.params), request.params.model);
211
+ }
209
212
  if (request.method === 'set_input_mode') {
210
213
  return kernel.setInputMode(requestTarget(request.params), request.params.mode);
211
214
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "newmark-agent",
3
3
  "productName": "Newmark Agent",
4
- "version": "0.4.3",
4
+ "version": "0.4.4",
5
5
  "description": "Newmark Agent — Portable AI coding agent with rich GUI and CLI (TypeScript)",
6
6
  "homepage": "https://github.com/positer/Newmark-Agent",
7
7
  "repository": {
@@ -33,7 +33,7 @@
33
33
  "start:dev": "npm run build && electron .",
34
34
  "start:cli": "node dist/launcher.js --cli",
35
35
  "test:tui": "npm run build && npm run test:tui:built",
36
- "test:tui:built": "node ../TUI/test/demo.test.js && node ../TUI/test/cursor-follow-stress.test.js && node dist/tests/tuiLauncherVerify.js && node dist/tests/tuiStopRaceVerify.js",
36
+ "test:tui:built": "node ../TUI/test/demo.test.js && node ../TUI/test/cursor-follow-stress.test.js && node ../TUI/test/cursor-follow-pty-gate.js && node dist/tests/tuiLauncherVerify.js && node dist/tests/tuiStopRaceVerify.js",
37
37
  "test:ssh-tui-stress": "npm run build && npm run test:ssh-tui-stress:built",
38
38
  "test:ssh-tui-stress:built": "node scripts/release-ssh-tui-stress.cjs",
39
39
  "test:wsl-tui-stress:built": "node scripts/release-wsl-tui-stress.cjs",
@@ -53,7 +53,7 @@
53
53
  "test:desktop": "npm run build && npm run test:desktop:built",
54
54
  "test:deletion-safety": "npm run build && npm run test:deletion-safety:built",
55
55
  "test:deletion-safety:built": "node scripts/deletion-safety-stress.cjs",
56
- "test:desktop:built": "node dist/tests/verify.js && node dist/tests/thinkingTierMapCacheStressVerify.js && node dist/tests/computerUseSessionVerify.js && node dist/tests/contextSystemV2Verify.js && node dist/tests/contextSystemV2StressVerify.js && node dist/tests/providerAdapterV2Verify.js && node dist/tests/providerTimeoutRecoveryVerify.js && node dist/tests/agentRuntimeV2Verify.js && node dist/tests/toolchainExposureV2Verify.js && node dist/tests/localOcrFallbackVerify.js && node dist/tests/dev018ModeSubagentStressVerify.js && node dist/tests/conversationBranchStressVerify.js && node dist/tests/conversationArchiveConcurrencyVerify.js && node dist/tests/conversationArchiveRuntimeVerify.js && node dist/tests/dev040ComprehensiveStressVerify.js && node dist/tests/memoryPolicyVerify.js && node dist/tests/newmarkSelectVerify.js && node dist/tests/mcpManagerVerify.js && node dist/tests/dshCompatibilityVerify.js && node dist/tests/performanceOptimizationVerify.js && node scripts/compression-pressure-stress.cjs && node dist/tests/compressionFidelityVerify.js && node dist/tests/responseTrajectoryVerify.js && node dist/tests/normalChatRegressionVerify.js && node dist/tests/dev008-subagent.js && node dist/tests/dev009Verify.js && node dist/tests/runtimeIsolationVerify.js && node dist/tests/runtimePoolCapacityVerify.js && node dist/tests/guideWorkRunVerify.js && node scripts/flow-pause-stop-draft-stress.cjs && node dist/tests/guideUiReconcileVerify.js && node dist/tests/queueAttachmentIsolationVerify.js && node dist/tests/workspaceMenuVerify.js && node dist/tests/userImagePersistenceVerify.js && node dist/tests/displayImageVerify.js && node dist/tests/visualPreferencesVerify.js && node dist/tests/browserUseVerify.js && node dist/tests/toolProcessVerify.js && node dist/tests/toolProvisioningVerify.js && node dist/tests/taskToolsVerify.js && node dist/tests/contextCacheHitStressVerify.js && node dist/tests/startupPrewarmVerify.js && node dist/tests/pdfPreviewServerVerify.js && node dist/tests/editorLifecycleVerify.js && node dist/tests/terminalTakeoverVerify.js && node dist/tests/workspaceFileRouterVerify.js && node dist/tests/computerUsePerformanceVerify.js && node dist/tests/autoRouterVerify.js && node dist/tests/autoAgentIntegrationVerify.js && node dist/tests/autoRouteRatingVerify.js && node dist/tests/providerIdentityVerify.js && node dist/tests/modelValidationVerify.js && node dist/tests/modelValidationAgentIntegrationVerify.js && node scripts/deletion-safety-stress.cjs",
56
+ "test:desktop:built": "node dist/tests/verify.js && node dist/tests/thinkingTierMapCacheStressVerify.js && node dist/tests/computerUseSessionVerify.js && node dist/tests/contextSystemV2Verify.js && node dist/tests/contextSystemV2StressVerify.js && node dist/tests/providerAdapterV2Verify.js && node dist/tests/providerTimeoutRecoveryVerify.js && node dist/tests/agentRuntimeV2Verify.js && node dist/tests/toolchainExposureV2Verify.js && node dist/tests/localOcrFallbackVerify.js && node dist/tests/dev018ModeSubagentStressVerify.js && node dist/tests/conversationBranchStressVerify.js && node dist/tests/conversationArchiveConcurrencyVerify.js && node dist/tests/conversationArchiveRuntimeVerify.js && node dist/tests/dev040ComprehensiveStressVerify.js && node dist/tests/memoryPolicyVerify.js && node dist/tests/newmarkSelectVerify.js && node dist/tests/mcpManagerVerify.js && node dist/tests/dshCompatibilityVerify.js && node dist/tests/performanceOptimizationVerify.js && node scripts/compression-pressure-stress.cjs && node dist/tests/compressionFidelityVerify.js && node dist/tests/responseTrajectoryVerify.js && node dist/tests/normalChatRegressionVerify.js && node dist/tests/dev008-subagent.js && node dist/tests/dev009Verify.js && node dist/tests/runtimeIsolationVerify.js && node dist/tests/runtimePoolCapacityVerify.js && node dist/tests/guideWorkRunVerify.js && node dist/tests/modelSwitchBehaviorVerify.js && node scripts/flow-pause-stop-draft-stress.cjs && node dist/tests/guideUiReconcileVerify.js && node dist/tests/queueAttachmentIsolationVerify.js && node dist/tests/workspaceMenuVerify.js && node dist/tests/userImagePersistenceVerify.js && node dist/tests/displayImageVerify.js && node dist/tests/visualPreferencesVerify.js && node dist/tests/browserUseVerify.js && node dist/tests/toolProcessVerify.js && node dist/tests/toolProvisioningVerify.js && node dist/tests/taskToolsVerify.js && node dist/tests/contextCacheHitStressVerify.js && node dist/tests/startupPrewarmVerify.js && node dist/tests/pdfPreviewServerVerify.js && node dist/tests/editorLifecycleVerify.js && node dist/tests/terminalTakeoverVerify.js && node dist/tests/workspaceFileRouterVerify.js && node dist/tests/computerUsePerformanceVerify.js && node dist/tests/autoRouterVerify.js && node dist/tests/autoAgentIntegrationVerify.js && node dist/tests/autoRouteRatingVerify.js && node dist/tests/providerIdentityVerify.js && node dist/tests/modelValidationVerify.js && node dist/tests/modelValidationAgentIntegrationVerify.js && node scripts/deletion-safety-stress.cjs",
57
57
  "test:conversation-branch-stress": "npm run build && node dist/tests/conversationBranchStressVerify.js",
58
58
  "test:conversation-archive-concurrency": "npm run build && node dist/tests/conversationArchiveConcurrencyVerify.js",
59
59
  "test:memory-policy": "npm run build && node dist/tests/memoryPolicyVerify.js",