newmark-agent 0.4.2 → 0.4.3

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.
@@ -586,7 +586,40 @@ class ConversationKernel {
586
586
  if (runtime.stopRequestedRunId === runtime.runId)
587
587
  return this.result(runtime, lastTokens);
588
588
  const next = runtime.pendingNextTurn.shift();
589
- lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
589
+ if (next.queueMode === 'steer' && typeof next.message !== 'string' && !!next.message.clientMessageId) {
590
+ const batchGuides = [];
591
+ const pushGuide = (message) => {
592
+ batchGuides.push({
593
+ clientMessageId: String(message.clientMessageId || ''),
594
+ guideId: message.guideId,
595
+ text: message.text,
596
+ images: message.images?.map(image => ({ ...image })),
597
+ attachments: message.attachments?.map(attachment => ({ ...attachment })),
598
+ });
599
+ };
600
+ pushGuide(next.message);
601
+ while (runtime.pendingNextTurn.length > 0
602
+ && runtime.pendingNextTurn[0].queueMode === 'steer'
603
+ && typeof runtime.pendingNextTurn[0].message !== 'string'
604
+ && !!runtime.pendingNextTurn[0].message.clientMessageId) {
605
+ const guide = runtime.pendingNextTurn.shift();
606
+ pushGuide(guide.message);
607
+ }
608
+ if (batchGuides.length === 1) {
609
+ lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
610
+ continue;
611
+ }
612
+ const batchText = batchGuides.map((guide, index) => `Guide ${index + 1}: ${guide.text}`).join('\n');
613
+ const batchMessage = {
614
+ text: `Apply the following intervening Guides in submission order within the current Build Block and continue automatically:\n${batchText}`,
615
+ hiddenUserInput: true,
616
+ batchGuides,
617
+ };
618
+ lastTokens = await this.runSingle(runtime, batchMessage, 'steer');
619
+ }
620
+ else {
621
+ lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
622
+ }
590
623
  }
591
624
  const rootMessage = runtime.runner.subagents.readRootInbox()[0];
592
625
  if (!rootMessage) {
@@ -23,6 +23,8 @@ const MODE_SCOPED_TOOLS = new Set([
23
23
  'read_tool_result',
24
24
  'goal_manage',
25
25
  'conversation_rename',
26
+ 'task_read',
27
+ 'task_create',
26
28
  'question',
27
29
  'task',
28
30
  'subagent_list',
@@ -36,6 +38,7 @@ const MODE_SCOPED_TOOLS = new Set([
36
38
  'branch_create',
37
39
  ]);
38
40
  const PLAN_READ_ONLY_TOOLS = new Set([
41
+ 'task_read',
39
42
  'pwd',
40
43
  'read',
41
44
  'glob',
@@ -29,13 +29,25 @@ export declare class LLMProvider {
29
29
  openAIMode: OpenAITransportMode | boolean;
30
30
  useProviderAdaptersV2: boolean;
31
31
  requestTimeoutMs: number;
32
+ /** dev-0.4.3 模型原生思考强度档位映射(模型名 → { 模型原生档位: Newmark 档位 })。 */
33
+ thinkingTierMaps?: Record<string, Record<string, string>> | undefined;
32
34
  static nodeHttpTransport: ((method: 'GET' | 'POST', url: string, headers: Record<string, string>, body?: string) => Promise<NodeHttpResult>) | null;
33
35
  static powershellTransport: ((method: 'GET' | 'POST', url: string, headers: Record<string, string>, body?: string) => Promise<NodeHttpResult>) | null;
34
- constructor(name: string, baseUrl: string, apiKey: string, explicitProtocol?: ProviderProtocol | undefined, openAIMode?: OpenAITransportMode | boolean, useProviderAdaptersV2?: boolean, requestTimeoutMs?: number);
36
+ constructor(name: string, baseUrl: string, apiKey: string, explicitProtocol?: ProviderProtocol | undefined, openAIMode?: OpenAITransportMode | boolean, useProviderAdaptersV2?: boolean, requestTimeoutMs?: number,
37
+ /** dev-0.4.3 模型原生思考强度档位映射(模型名 → { 模型原生档位: Newmark 档位 })。 */
38
+ thinkingTierMaps?: Record<string, Record<string, string>> | undefined);
35
39
  private effectiveRequestTimeout;
36
40
  private withRequestTimeout;
37
41
  intelligenceConfig(tier: string): IntelligenceConfig;
38
42
  private reasoningEffort;
43
+ /**
44
+ * dev-0.4.3 模型原生思考强度档位映射。不同模型的原生 reasoning_effort
45
+ * 档位配置可能不同(档位数量或档位命名不同)。模型配置 `thinking_tier_map`
46
+ * 以「模型原生档位名 → Newmark 档位」声明映射,这里把 Newmark 档位反查为
47
+ * 模型原生档位名;未配置映射(或映射为空)时返回 undefined,由调用方
48
+ * 维持默认透传行为(默认不变动映射)。
49
+ */
50
+ private mappedNativeEffort;
39
51
  private applyChatReasoningEffort;
40
52
  private protocol;
41
53
  private openAITransportMode;
@@ -98,9 +98,12 @@ class LLMProvider {
98
98
  openAIMode;
99
99
  useProviderAdaptersV2;
100
100
  requestTimeoutMs;
101
+ thinkingTierMaps;
101
102
  static nodeHttpTransport = null;
102
103
  static powershellTransport = null;
103
- constructor(name, baseUrl, apiKey, explicitProtocol, openAIMode = 'chat_stream', useProviderAdaptersV2 = false, requestTimeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS) {
104
+ constructor(name, baseUrl, apiKey, explicitProtocol, openAIMode = 'chat_stream', useProviderAdaptersV2 = false, requestTimeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS,
105
+ /** dev-0.4.3 模型原生思考强度档位映射(模型名 → { 模型原生档位: Newmark 档位 })。 */
106
+ thinkingTierMaps) {
104
107
  this.name = name;
105
108
  this.baseUrl = baseUrl;
106
109
  this.apiKey = apiKey;
@@ -108,6 +111,7 @@ class LLMProvider {
108
111
  this.openAIMode = openAIMode;
109
112
  this.useProviderAdaptersV2 = useProviderAdaptersV2;
110
113
  this.requestTimeoutMs = requestTimeoutMs;
114
+ this.thinkingTierMaps = thinkingTierMaps;
111
115
  }
112
116
  effectiveRequestTimeout(timeoutMs) {
113
117
  const requested = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS;
@@ -140,6 +144,9 @@ class LLMProvider {
140
144
  }
141
145
  }
142
146
  reasoningEffort(model, tier) {
147
+ const mapped = this.mappedNativeEffort(model, tier);
148
+ if (mapped !== undefined)
149
+ return mapped;
143
150
  if (!/^(?:gpt-5|o[134](?:-|$)|codex)|(?:reasoner|reasoning|deepseek-r1|deepseek-reasoner|\br1\b)/i.test(model))
144
151
  return undefined;
145
152
  const effort = tier === 'low' || tier === 'high' || tier === 'xhigh' || tier === 'max'
@@ -150,6 +157,39 @@ class LLMProvider {
150
157
  // OpenAI-compatible/Codex gateways may expose the user-facing max tier.
151
158
  return effort === 'max' && /^https:\/\/(?:api\.)?openai\.com(?:\/|$)/i.test(this.cleanBaseUrl()) ? 'xhigh' : effort;
152
159
  }
160
+ /**
161
+ * dev-0.4.3 模型原生思考强度档位映射。不同模型的原生 reasoning_effort
162
+ * 档位配置可能不同(档位数量或档位命名不同)。模型配置 `thinking_tier_map`
163
+ * 以「模型原生档位名 → Newmark 档位」声明映射,这里把 Newmark 档位反查为
164
+ * 模型原生档位名;未配置映射(或映射为空)时返回 undefined,由调用方
165
+ * 维持默认透传行为(默认不变动映射)。
166
+ */
167
+ mappedNativeEffort(model, tier) {
168
+ const map = this.thinkingTierMaps?.[model];
169
+ if (!map || typeof map !== 'object')
170
+ return undefined;
171
+ const order = ['low', 'medium', 'high', 'xhigh', 'max'];
172
+ const entries = Object.entries(map)
173
+ .filter((entry) => order.includes(entry[1]))
174
+ .sort((a, b) => order.indexOf(a[1]) - order.indexOf(b[1]));
175
+ if (!entries.length)
176
+ return undefined;
177
+ const normalized = tier === 'ultra'
178
+ ? 'max'
179
+ : (order.includes(tier) ? tier : 'medium');
180
+ const exact = entries.find(([, newmark]) => newmark === normalized);
181
+ if (exact)
182
+ return exact[0];
183
+ // 就近降级:取强度不超过目标档位的最高已映射档位
184
+ const targetIndex = order.indexOf(normalized);
185
+ for (let i = targetIndex; i >= 0; i--) {
186
+ const candidate = entries.find(([, newmark]) => newmark === order[i]);
187
+ if (candidate)
188
+ return candidate[0];
189
+ }
190
+ // 全部高于目标档位:取最低档位
191
+ return entries[0]?.[0];
192
+ }
153
193
  applyChatReasoningEffort(body, model, tier) {
154
194
  const effort = this.reasoningEffort(model, tier);
155
195
  if (effort)
@@ -859,6 +899,7 @@ class LLMProvider {
859
899
  tools: this.toNormalizedTools(tools),
860
900
  temperature,
861
901
  maxOutputTokens: maxTokens,
902
+ reasoningEffort: this.reasoningEffort(model, reasoningTier),
862
903
  apiKey: this.apiKey,
863
904
  baseUrl: this.cleanBaseUrl(),
864
905
  ...(sessionId ? { sessionId } : {}),
@@ -52,7 +52,9 @@ export interface NormalizedAgentRequest {
52
52
  tools: NormalizedTool[];
53
53
  temperature: number;
54
54
  maxOutputTokens: number;
55
- reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
55
+ /** dev-0.4.3:模型原生思考强度档位名。Newmark 档位经模型配置
56
+ * `thinking_tier_map` 映射后可能不再是 Newmark 档位名,故为自由字符串。 */
57
+ reasoningEffort?: string;
56
58
  apiKey: string;
57
59
  baseUrl: string;
58
60
  /** 可选的会话标识。仅当目标 provider 显式支持 session_id 语义的上下文缓存
@@ -372,7 +372,9 @@ class ToolExecutor {
372
372
  t('background_tool', 'Run a tool call in the background WITHOUT blocking the conversation turn. Pass the target tool name and its arguments; this tool returns a background_id IMMEDIATELY, and the real tool keeps running in the background. The result is persisted and can be retrieved later with read_tool_result. Use this for long-running or non-critical tools (bash, web_fetch, long read/grep) so the conversation continues without waiting. The background result stays OUT of context until you explicitly read it, preserving prompt-cache hit rate. Orchestration/flow/subagent/question tools cannot be backgrounded.', { tool: { type: 'string', description: 'The tool name to run in the background (e.g. bash, web_fetch, read, grep).' }, args: { type: 'object', description: 'The arguments object for the target tool, matching its normal schema.' } }, ['tool']),
373
373
  t('read_tool_result', 'Read the result of a background tool. Pass the background_id returned by background_tool. When status is running, returns a running marker; when done, returns the persisted result (optionally release it from storage after reading); when error, returns the failure. Background results are released from storage only when you set release=true.', { background_id: { type: 'string', description: 'The background_id returned by background_tool.' }, release: { type: 'boolean', description: 'Set true to release the persisted result from storage after reading it.' } }, ['background_id']),
374
374
  t('goal_manage', 'Actively manage the persistent Goal state for this conversation. You may enter Goal mode, update (edit) its objective, mark it complete, or exit Goal mode yourself. Call this when the user asks you to pursue a persistent objective, when the objective changes, when you have verified the objective is genuinely achieved, or when you judge the Goal is no longer needed and should be cleared. This is the agent-side state control that mirrors the GUI goal panel controls. enter/update require objective; complete marks the objective verified and exits Goal mode; exit clears the Goal (and returns to Build mode) without claiming completion.', { action: { type: 'string', enum: ['enter', 'update', 'complete', 'exit'], description: 'enter=enter Goal mode and set the objective; update=edit the objective (records a change); complete=mark the objective verified-achieved and exit Goal mode; exit=clear the Goal and return to Build mode without claiming completion.' }, objective: { type: 'string', description: 'The Goal objective text. Required for enter and update.' }, reason: { type: 'string', description: 'Optional one-line reason for the state change, recorded for audit.' } }, ['action']),
375
- t('conversation_rename', 'Rename the CURRENT conversation to a concise, descriptive title you choose. On the FIRST Build Block of a NEW conversation the runtime asks you to call this once so the conversation list shows a meaningful name instead of an auto-generated one. Keep the title short (a few words) and cache-friendly: a concrete noun phrase describing the task, never a sentence or quoted prompt.', { title: { type: 'string', description: 'The new conversation title (a short noun phrase, e.g. "Fix TUI color leak", "Add goal_manage tool").' } }, ['title']),
375
+ t('task_read', 'Read the persistent inline task checklist of the CURRENT conversation (the same list the GUI Task panel and TUI plan view render). Returns bounded items (id, status, task text <=240 chars) plus unfinished count. Read-only and side-effect free; call this whenever you need concrete task-list state instead of assuming it. Kept out of the system prompt so provider prefix caching stays stable.', {}, []),
376
+ t('task_create', 'Maintain the persistent inline task checklist of the CURRENT conversation. This is the durable replacement for ephemeral in-reply checklists: items you create here appear in the GUI Task panel and TUI plan view immediately and persist across Build Blocks. Actions: create appends one task item; update changes status (pending|in_progress|done) or text of one item by id or index; clear removes completed items. Use create when starting a multi-step task, update as each item progresses, and update status=done when verified. Returns a compact confirmation, never the full list.', { action: { type: 'string', enum: ['create', 'update', 'clear'], description: 'create=append a task item; update=change one item status/text; clear=remove completed items.' }, task: { type: 'string', description: 'Task text for create, or new text for update. Short actionable label (<=400 chars).' }, text: { type: 'string', description: 'Alias of task.' }, id: { type: 'string', description: 'Item id from task_read for update.' }, index: { type: 'number', description: '0-based item index for update (alternative to id).' }, status: { type: 'string', enum: ['pending', 'in_progress', 'done', 'blocked'], description: 'New status for update; blocked is stored as pending.' } }, ['action']),
377
+ t('conversation_rename', 'Rename the CURRENT conversation to a concise, descriptive title you choose. Use this when the user asks to rename the conversation or when the current auto-generated title no longer describes the work. Keep the title short (a few words) and cache-friendly: a concrete noun phrase describing the task, never a sentence or quoted prompt.', { title: { type: 'string', description: 'The new conversation title (a short noun phrase, e.g. "Fix TUI color leak", "Add goal_manage tool").' } }, ['title']),
376
378
  t('question', 'Ask user a multiple-choice question', { questions: { type: 'array' } }, ['questions']),
377
379
  t('skill_download', 'Download a skill', { name: { type: 'string' }, source: { type: 'string' } }, ['name', 'source']),
378
380
  t('skill', 'Search enabled skill metadata or load one exact skill body on demand. Use query when unsure, then name to load the selected skill.', { query: { type: 'string', maxLength: 200 }, name: { type: 'string', maxLength: 200 } }, []),
@@ -430,19 +430,28 @@ function chatView(state, width, height, p) {
430
430
  `${p.bold}Conversations${p.reset}`,
431
431
  `${p.muted}N new${p.reset}`,
432
432
  "",
433
- ...state.snapshot.conversations.map((item, i) => {
434
- const style = state.focusRegion === "content" && i === state.selected && !state.inputMode ? `${p.selected}${p.bold}` : "";
435
- const running = state.runningConversationKeys?.has(`${state.target.workspaceId}::${item.id}`);
436
- const marker = running
437
- ? `${p.cyan}${["\\", "—", "/", "—"][state.tick % 4]}${p.reset}`
438
- : item.title === state.lastConversation
439
- ? `${p.cyan}›${p.reset}`
440
- : item.pinned
441
- ? `${p.amber}◆${p.reset}`
442
- : "·";
443
- const updated = String(item.updatedAt || "").slice(11, 16) || "—";
444
- return `${style}${marker} ${pad(item.title, 19)} ${p.muted}${updated}${p.reset}`;
445
- })
433
+ ...(() => {
434
+ const conversationRows = state.snapshot.conversations.map((item, i) => {
435
+ const style = state.focusRegion === "content" && i === state.selected && !state.inputMode ? `${p.selected}${p.bold}` : "";
436
+ const running = state.runningConversationKeys?.has(`${state.target.workspaceId}::${item.id}`);
437
+ const marker = running
438
+ ? `${p.cyan}${["\\", "—", "/", "—"][state.tick % 4]}${p.reset}`
439
+ : item.title === state.lastConversation
440
+ ? `${p.cyan}›${p.reset}`
441
+ : item.pinned
442
+ ? `${p.amber}◆${p.reset}`
443
+ : "·";
444
+ const updated = String(item.updatedAt || "").slice(11, 16) || "—";
445
+ return `${style}${marker} ${pad(item.title, 19)} ${p.muted}${updated}${p.reset}`;
446
+ });
447
+ const listViewport = Math.max(1, height - 3);
448
+ const listMaxScroll = Math.max(0, conversationRows.length - listViewport);
449
+ let listScroll = Math.max(0, Math.min(listMaxScroll, Number(state.conversationListScroll) || 0));
450
+ if (state.selected < listScroll) listScroll = state.selected;
451
+ else if (state.selected >= listScroll + listViewport) listScroll = state.selected - listViewport + 1;
452
+ state.conversationListScroll = Math.max(0, Math.min(listMaxScroll, listScroll));
453
+ return conversationRows.slice(state.conversationListScroll, state.conversationListScroll + listViewport);
454
+ })()
446
455
  ] : [];
447
456
  const preview = state.snapshot.conversations[state.selected] || state.snapshot.conversations[0] || {
448
457
  id: state.target.conversationId,
@@ -736,20 +745,19 @@ function flowListView(state, width, p) {
736
745
 
737
746
  function flowTaskView(state, width, p) {
738
747
  const components = state.currentFlow?.components || [];
739
- return [
748
+ const rows = [
740
749
  ...conversationContext(state, p, "Flow task"),
741
750
  `${p.bold}${tr(state, "Flow Task")}${p.reset} ${p.muted}${state.currentFlow?.name || "No workflow"}${p.reset}`,
742
- "",
743
- ...components.flatMap((component, index) => {
744
- const style = state.focusRegion === "content" && index === state.selected ? `${p.selected}${p.bold}` : "";
745
- if (style) state.contentFocusLine = 6 + index * 2;
746
- const mode = component.type === "logic" ? "LOGIC" : String(component.mode || "BUILD").toUpperCase();
747
- return [
748
- `${style} ${String(component.id).padStart(2)} ${pad(mode, 7)} ${truncate(component.prompt, Math.max(18, width - 16))}`,
749
- component.type === "logic" ? ` ${p.muted}true → ${component.goto_true} · false → ${component.goto_false}${p.reset}` : ""
750
- ].filter(Boolean);
751
- })
751
+ ""
752
752
  ];
753
+ components.forEach((component, index) => {
754
+ const style = state.focusRegion === "content" && index === state.selected ? `${p.selected}${p.bold}` : "";
755
+ if (style) state.contentFocusLine = rows.length;
756
+ const mode = component.type === "logic" ? "LOGIC" : String(component.mode || "BUILD").toUpperCase();
757
+ rows.push(`${style} ${String(component.id).padStart(2)} ${pad(mode, 7)} ${truncate(component.prompt, Math.max(18, width - 16))}`);
758
+ if (component.type === "logic") rows.push(` ${p.muted}true → ${component.goto_true} · false → ${component.goto_false}${p.reset}`);
759
+ });
760
+ return rows;
753
761
  }
754
762
 
755
763
  function toolsView(state, width, p) {
@@ -1143,11 +1151,19 @@ function overlayLines(state, width, p) {
1143
1151
  }
1144
1152
  if (state.overlay === "palette") {
1145
1153
  const commands = filteredCommands(state);
1154
+ const paletteViewport = Math.min(7, Math.max(1, commands.length));
1155
+ const paletteMaxScroll = Math.max(0, commands.length - paletteViewport);
1156
+ let paletteScroll = Math.max(0, Math.min(paletteMaxScroll, Number(state.paletteScroll) || 0));
1157
+ if (state.paletteIndex < paletteScroll) paletteScroll = state.paletteIndex;
1158
+ else if (state.paletteIndex >= paletteScroll + paletteViewport) paletteScroll = state.paletteIndex - paletteViewport + 1;
1159
+ state.paletteScroll = Math.max(0, Math.min(paletteMaxScroll, paletteScroll));
1160
+ const visibleCommands = commands.slice(state.paletteScroll, state.paletteScroll + paletteViewport);
1146
1161
  const rows = [
1147
1162
  `${p.bold}>${p.reset} ${state.paletteQuery}${p.cyan}▏${p.reset}`,
1148
1163
  `${p.muted}${"─".repeat(Math.max(8, Math.min(64, width - 10)))}${p.reset}`,
1149
- ...(commands.length ? commands.slice(0, 7).map((command, i) => {
1150
- const style = i === state.paletteIndex ? `${p.selected}${p.bold}` : "";
1164
+ ...(visibleCommands.length ? visibleCommands.map((command, i) => {
1165
+ const commandIndex = state.paletteScroll + i;
1166
+ const style = commandIndex === state.paletteIndex ? `${p.selected}${p.bold}` : "";
1151
1167
  return `${style} ${pad(command.label, Math.max(18, Math.min(52, width - 18)))} ${p.muted}${command.hint}${p.reset}`;
1152
1168
  }) : [`${p.muted} No matching commands${p.reset}`]),
1153
1169
  "",
@@ -1156,14 +1172,22 @@ function overlayLines(state, width, p) {
1156
1172
  return card(rows, Math.min(72, width - 4), p, "Command palette");
1157
1173
  }
1158
1174
  if (state.overlay === "flow-select") {
1175
+ const flowViewport = Math.min(7, Math.max(1, state.flows.length));
1176
+ const flowMaxScroll = Math.max(0, state.flows.length - flowViewport);
1177
+ let flowScroll = Math.max(0, Math.min(flowMaxScroll, Number(state.flowSelectionScroll) || 0));
1178
+ if (state.flowSelectionIndex < flowScroll) flowScroll = state.flowSelectionIndex;
1179
+ else if (state.flowSelectionIndex >= flowScroll + flowViewport) flowScroll = state.flowSelectionIndex - flowViewport + 1;
1180
+ state.flowSelectionScroll = Math.max(0, Math.min(flowMaxScroll, flowScroll));
1181
+ const visibleFlows = state.flows.slice(state.flowSelectionScroll, state.flowSelectionScroll + flowViewport);
1159
1182
  return card([
1160
1183
  `${p.bold}Flow mode requires a workflow${p.reset}`,
1161
1184
  `${p.muted}This selection is bound to ${state.lastConversation}.${p.reset}`,
1162
1185
  "",
1163
- ...(state.flows.length
1164
- ? state.flows.map((name, index) => {
1165
- const style = index === state.flowSelectionIndex ? `${p.selected}${p.bold}` : "";
1166
- return `${style} ${index === state.flowSelectionIndex ? "›" : " "} ${name}${p.reset}`;
1186
+ ...(visibleFlows.length
1187
+ ? visibleFlows.map((name, i) => {
1188
+ const flowIndex = state.flowSelectionScroll + i;
1189
+ const style = flowIndex === state.flowSelectionIndex ? `${p.selected}${p.bold}` : "";
1190
+ return `${style} ${flowIndex === state.flowSelectionIndex ? "›" : " "} ${name}${p.reset}`;
1167
1191
  })
1168
1192
  : [`${p.amber}No workflows are configured in ~/.Newmark/Flow.${p.reset}`]),
1169
1193
  "",
@@ -183,6 +183,8 @@ function createState(options = {}) {
183
183
  workflowDraft: { name: "", mode: "build", prompt: "" },
184
184
  workflowFormIndex: 0,
185
185
  flowSelectionIndex: 0,
186
+ flowSelectionScroll: 0,
187
+ conversationListScroll: 0,
186
188
  flowByConversation: initialFlow ? { [`${target.workspaceId}::${target.conversationId}`]: initialFlow } : {},
187
189
  currentFlow: initialFlow,
188
190
  theme: appearance.theme,
@@ -192,6 +194,7 @@ function createState(options = {}) {
192
194
  settingChoiceIndex: 0,
193
195
  paletteQuery: "",
194
196
  paletteIndex: 0,
197
+ paletteScroll: 0,
195
198
  inputMode: false,
196
199
  input: "",
197
200
  inputCursor: 0,
@@ -3523,8 +3523,6 @@ button.todo-item { background: transparent; }
3523
3523
  .context-inspector-head { display:flex; align-items:flex-start; gap:8px; margin-bottom:10px; }
3524
3524
  .context-inspector-title { flex:1; min-width:0; color:var(--text-bright); font-size:12px; font-weight:700; }
3525
3525
  .context-inspector-subtitle { margin-top:2px; color:var(--text-dim); font-size:9px; }
3526
- .context-inspector-close { width:22px; height:22px; padding:0; border:0; border-radius:var(--radius-sm); background:transparent; color:var(--text-dim); cursor:pointer; }
3527
- .context-inspector-close:hover, .context-inspector-close:focus-visible { color:var(--text-bright); background:var(--control-hover-bg); outline:none; }
3528
3526
  .context-inspector-meter { height:6px; margin:8px 0 10px; overflow:hidden; border-radius:var(--radius-full); background:rgba(168,168,168,.18); }
3529
3527
  .context-inspector-meter-fill { height:100%; border-radius:inherit; background:var(--accent); transition:width var(--duration-normal) var(--ease-out-expo), background var(--duration-fast) var(--ease-out-expo); }
3530
3528
  .context-inspector-grid { display:grid; grid-template-columns:1fr 1fr; gap:7px; }
@@ -6485,6 +6483,8 @@ var NEWMARK_I18N = {
6485
6483
  'model.contextSize': 'Context size',
6486
6484
  'model.vision': 'Vision',
6487
6485
  'model.thinking': 'Thinking',
6486
+ 'model.thinkingTierMap': 'Thinking tier map',
6487
+ 'model.thinkingTierMapHelp': 'Map native model effort tiers to Newmark tiers. One "native=Newmark" per line (e.g. minimal=low, balanced=medium, deep=high). Leave empty to keep the default mapping (Newmark tier names are sent as-is).',
6488
6488
  'model.description': 'Description',
6489
6489
  'model.optionalDescription': 'Optional description',
6490
6490
  'model.fuzzyInput': 'API key / provider details',
@@ -6847,6 +6847,8 @@ var NEWMARK_I18N = {
6847
6847
  'status.contextCompressed': 'Context compressed',
6848
6848
  'status.contextCompression': 'Context compression',
6849
6849
  'status.contextTokens': 'Context tokens',
6850
+ 'status.conversationTokens': 'Conversation tokens',
6851
+ 'status.cacheHitRate': 'Cache hit rate',
6850
6852
  'status.contextNearLimit': 'Context near limit',
6851
6853
  'status.contextOverLimit': 'Context over limit',
6852
6854
  'status.contextModeFallback': 'fallback',
@@ -6854,7 +6856,7 @@ var NEWMARK_I18N = {
6854
6856
  'status.messages': 'messages',
6855
6857
  'status.noCompression': 'No compression event in this conversation',
6856
6858
  'status.contextInspector': 'Context management',
6857
- 'status.contextInspectorHint': 'DSH-inspired live budget; visible chat history stays unchanged.',
6859
+ 'status.contextInspectorHint': 'Live context budget; visible chat history stays unchanged.',
6858
6860
  'status.activeBuild': 'Active Build',
6859
6861
  'status.longHistory': 'Long history',
6860
6862
  'status.trigger': 'Trigger',
@@ -7187,6 +7189,8 @@ var NEWMARK_I18N = {
7187
7189
  'model.contextSize': '上下文规模',
7188
7190
  'model.vision': '视觉',
7189
7191
  'model.thinking': '思考',
7192
+ 'model.thinkingTierMap': '思考强度档位映射',
7193
+ 'model.thinkingTierMapHelp': '将模型原生思考强度档位映射到 Newmark 档位。每行一个「原生档位=Newmark档位」(如 minimal=low、balanced=medium、deep=high)。留空表示不变动映射(按 Newmark 档位名原样发送)。',
7190
7194
  'model.description': '描述',
7191
7195
  'model.optionalDescription': '可选描述',
7192
7196
  'model.fuzzyInput': 'API key / 供应商信息',
@@ -7549,6 +7553,8 @@ var NEWMARK_I18N = {
7549
7553
  'status.contextCompressed': '上下文已压缩',
7550
7554
  'status.contextCompression': '上下文压缩',
7551
7555
  'status.contextTokens': '上下文 token',
7556
+ 'status.conversationTokens': '全对话 token',
7557
+ 'status.cacheHitRate': '缓存命中率',
7552
7558
  'status.contextNearLimit': '上下文接近上限',
7553
7559
  'status.contextOverLimit': '上下文已超限',
7554
7560
  'status.contextModeFallback': 'fallback',
@@ -7556,7 +7562,7 @@ var NEWMARK_I18N = {
7556
7562
  'status.messages': '条消息',
7557
7563
  'status.noCompression': '当前对话暂无压缩事件',
7558
7564
  'status.contextInspector': '上下文管理',
7559
- 'status.contextInspectorHint': '借鉴 DSH 的动态预算;可见对话历史不会被改写。',
7565
+ 'status.contextInspectorHint': '实时上下文预算;可见对话历史不会被改写。',
7560
7566
  'status.activeBuild': '当前 Build',
7561
7567
  'status.longHistory': '长期历史',
7562
7568
  'status.trigger': '触发线',
@@ -10174,7 +10180,7 @@ function renderWorkToolGroup(event, eventIndex) {
10174
10180
  var edited = workToolEditedFile(item);
10175
10181
  if (edited) {
10176
10182
  var editKey = 'edit:' + String(item.id || item.toolCallId || itemIndex) + ':' + edited.path;
10177
- return '<details class="conversation-work-file conversation-work-file-inline" open data-work-detail-key="' + escAttr(editKey) + '"><summary>' +
10183
+ return '<details class="conversation-work-file conversation-work-file-inline" data-work-detail-key="' + escAttr(editKey) + '"><summary>' +
10178
10184
  iconSvg('pencil', 'edited', 'tiny') +
10179
10185
  '<span class="conversation-work-file-name" title="' + escAttr(edited.path) + '">' + esc(currentLang() === 'zh' ? '已编辑 ' : 'Edited ') + esc(edited.name) + '</span>' +
10180
10186
  '<span class="conversation-work-file-stats"><span class="conversation-work-file-add">+' + edited.added + '</span><span class="conversation-work-file-del">-' + edited.deleted + '</span></span>' +
@@ -12075,6 +12081,12 @@ window.renderContextInspector = function() {
12075
12081
  var historyTrigger = contextInspectorValue(c.longHistoryTriggerTokens);
12076
12082
  var activeRetention = contextInspectorValue(c.buildBlockRetentionTokens);
12077
12083
  var historyRetention = contextInspectorValue(c.longHistoryRetentionTokens);
12084
+ var providerTotalTokens = contextInspectorValue(c.providerTotalTokens);
12085
+ var providerInputTokens = contextInspectorValue(c.providerInputTokens);
12086
+ var providerOutputTokens = contextInspectorValue(c.providerOutputTokens);
12087
+ var providerCacheReadTokens = contextInspectorValue(c.providerCacheReadTokens);
12088
+ var providerCacheReadRatio = Math.max(0, Math.min(1, Number(c.providerCacheReadRatio) || 0));
12089
+ var cacheHitPercent = Math.round(providerCacheReadRatio * 1000) / 10;
12078
12090
  var percent = Math.min(100, Math.round((used / max) * 100));
12079
12091
  var fillColor = c.warning === 'over_limit' ? 'var(--nm-state-danger)' : (c.warning === 'near_limit' ? 'var(--nm-state-warning)' : 'var(--accent)');
12080
12092
  var compression = state.contextCompression || null;
@@ -12083,12 +12095,13 @@ window.renderContextInspector = function() {
12083
12095
  : t('status.noCompressionShort');
12084
12096
  var busy = !!state.contextMutationPending;
12085
12097
  var running = typeof isCurrentConversationRunning === 'function' && isCurrentConversationRunning();
12086
- panel.innerHTML = '<div class="context-inspector-head"><div><div class="context-inspector-title">' + esc(t('status.contextInspector')) + '</div><div class="context-inspector-subtitle">' + esc(t('status.contextInspectorHint')) + '</div></div>' +
12087
- '<button type="button" class="context-inspector-close" onclick="window.closeContextInspector()" aria-label="' + esc(t('common.close') || 'Close') + '">×</button></div>' +
12098
+ panel.innerHTML = '<div class="context-inspector-head"><div><div class="context-inspector-title">' + esc(t('status.contextInspector')) + '</div><div class="context-inspector-subtitle">' + esc(t('status.contextInspectorHint')) + '</div></div></div>' +
12088
12099
  '<div class="context-inspector-meter"><div class="context-inspector-meter-fill" style="width:' + percent + '%;background:' + fillColor + '"></div></div>' +
12089
12100
  '<div class="context-inspector-grid">' +
12090
12101
  contextInspectorCell(t('status.contextTokens'), used + ' / ' + max) +
12102
+ contextInspectorCell(t('status.conversationTokens'), providerTotalTokens + ' · ' + providerInputTokens + ' in / ' + providerOutputTokens + ' out') +
12091
12103
  contextInspectorCell(t('status.activeBuild'), active + ' / ' + activeTrigger) +
12104
+ contextInspectorCell(t('status.cacheHitRate'), cacheHitPercent + '% · ' + providerCacheReadTokens + ' cached') +
12092
12105
  contextInspectorCell(t('status.longHistory'), history + ' / ' + historyTrigger) +
12093
12106
  contextInspectorCell(t('status.retention'), activeRetention + ' + ' + historyRetention) +
12094
12107
  contextInspectorCell(t('status.hotCache'), contextInspectorValue(c.cacheEntries) + ' entries') +
@@ -12104,6 +12117,17 @@ window.closeContextInspector = function() {
12104
12117
  window.renderContextInspector();
12105
12118
  };
12106
12119
 
12120
+ (function() {
12121
+ document.addEventListener('click', function(event) {
12122
+ if (!state.contextInspectorOpen) return;
12123
+ var ring = document.getElementById('context-token-ring');
12124
+ var panel = document.getElementById('context-inspector');
12125
+ if (!ring || !panel) return;
12126
+ if (panel.contains(event.target) || ring.contains(event.target)) return;
12127
+ window.closeContextInspector();
12128
+ });
12129
+ })();
12130
+
12107
12131
  window.toggleContextInspector = function() {
12108
12132
  state.contextInspectorOpen = !state.contextInspectorOpen;
12109
12133
  window.hideContextWindowTooltip();
@@ -15329,9 +15353,11 @@ function renderModelSettings() {
15329
15353
  : (normalizedStatus === 'not checked' ? 'model-eval-pending' : 'model-eval-bad');
15330
15354
  var details = '';
15331
15355
  if (typeof modelEntry !== 'string') {
15356
+ var tierMapCount = modelEntry.thinking_tier_map && typeof modelEntry.thinking_tier_map === 'object' ? Object.keys(modelEntry.thinking_tier_map).length : 0;
15332
15357
  details = '<span class="model-eval-line">ctx ' + esc(modelEntry.max_tokens || '') +
15333
15358
  ' | ' + esc(t('model.vision')) + ' ' + (modelEntry.vision ? t('common.on') : t('common.off')) +
15334
15359
  ' | ' + esc(t('model.thinking')) + ' ' + (modelEntry.thinking ? t('common.on') : t('common.off')) +
15360
+ (tierMapCount ? ' | ' + esc(t('model.thinkingTierMap')) + ' ' + tierMapCount : '') +
15335
15361
  ' | ' + esc(t('model.cost')) + ' ' + esc(evaluation ? evaluation.cost_rating : '-') +
15336
15362
  ' | ' + esc(t('model.performanceShort')) + ' ' + esc(modelEntry.capability_rating || '-') +
15337
15363
  ' | ' + esc(t('model.speed')) + ' ' + esc(modelEntry.speed_rating || '-') + '</span>';
@@ -16040,9 +16066,31 @@ window.addModel = function() {
16040
16066
  '<div style="display:flex;gap:10px;margin-bottom:10px;"><label style="font-size:11px;color:var(--text-dim);"><input type="checkbox" id="new-model-vision"> ' + esc(t('model.vision')) + '</label>' +
16041
16067
  '<label style="font-size:11px;color:var(--text-dim);"><input type="checkbox" id="new-model-thinking"> ' + esc(t('model.thinking')) + '</label></div>' +
16042
16068
  '<div class="auto-input-group"><label>' + esc(t('model.description')) + '</label><textarea id="new-model-desc" rows="2" placeholder="' + escAttr(t('model.optionalDescription')) + '"></textarea></div>' +
16069
+ '<div class="auto-input-group"><label>' + esc(t('model.thinkingTierMap')) + '</label><textarea id="new-model-tier-map" rows="3" placeholder="minimal=low&#10;balanced=medium&#10;deep=high"></textarea>' +
16070
+ '<div style="margin-top:10px;font-size:11px;color:var(--text-dim);">' + esc(t('model.thinkingTierMapHelp')) + '</div></div>' +
16043
16071
  '<div style="margin-top:10px;"><button class="sec-btn primary" onclick="window.saveNewModel()">' + esc(t('common.save')) + '</button></div>');
16044
16072
  };
16045
16073
 
16074
+ window.parseThinkingTierMap = function(text) {
16075
+ var map = {};
16076
+ if (!text) return map;
16077
+ String(text).split('\n').forEach(function(line) {
16078
+ var trimmed = line.trim();
16079
+ if (!trimmed) return;
16080
+ var eq = trimmed.indexOf('=');
16081
+ if (eq <= 0) return;
16082
+ var native = trimmed.slice(0, eq).trim();
16083
+ var newmark = trimmed.slice(eq + 1).trim();
16084
+ if (native && newmark) map[native] = newmark;
16085
+ });
16086
+ return map;
16087
+ };
16088
+
16089
+ window.serializeThinkingTierMap = function(map) {
16090
+ if (!map || typeof map !== 'object') return '';
16091
+ return Object.keys(map).map(function(k) { return k + '=' + map[k]; }).join('\n');
16092
+ };
16093
+
16046
16094
  window.saveNewModel = function() {
16047
16095
  var name = document.getElementById('new-model-name').value;
16048
16096
  var provIdx = parseInt(document.getElementById('new-model-provider').value);
@@ -16050,9 +16098,11 @@ window.saveNewModel = function() {
16050
16098
  var visionEl = document.getElementById('new-model-vision');
16051
16099
  var thinkingEl = document.getElementById('new-model-thinking');
16052
16100
  var descEl = document.getElementById('new-model-desc');
16101
+ var tierMapEl = document.getElementById('new-model-tier-map');
16053
16102
  if (name && !isNaN(provIdx) && state.providers[provIdx]) {
16054
16103
  if (!state.providers[provIdx].models) state.providers[provIdx].models = [];
16055
- state.providers[provIdx].models.push({
16104
+ var tierMap = window.parseThinkingTierMap(tierMapEl ? tierMapEl.value : '');
16105
+ var entry = {
16056
16106
  name: name,
16057
16107
  display: name,
16058
16108
  description: descEl ? descEl.value : '',
@@ -16069,7 +16119,9 @@ window.saveNewModel = function() {
16069
16119
  xhigh: { description: 'xhigh' },
16070
16120
  max: { description: 'max' }
16071
16121
  }
16072
- });
16122
+ };
16123
+ if (Object.keys(tierMap).length) entry.thinking_tier_map = tierMap;
16124
+ state.providers[provIdx].models.push(entry);
16073
16125
  api.saveConfig({providers: state.providers});
16074
16126
  window.refreshModelSelect();
16075
16127
  }
@@ -16089,6 +16141,8 @@ window.editModel = function(provIdx, modelIdx) {
16089
16141
  '<div style="display:flex;gap:10px;margin-bottom:10px;"><label style="font-size:11px;color:var(--text-dim);"><input type="checkbox" id="edit-model-vision"' + (model.vision ? ' checked' : '') + '> ' + esc(t('model.vision')) + '</label>' +
16090
16142
  '<label style="font-size:11px;color:var(--text-dim);"><input type="checkbox" id="edit-model-thinking"' + (model.thinking ? ' checked' : '') + '> ' + esc(t('model.thinking')) + '</label></div>' +
16091
16143
  '<div class="auto-input-group"><label>' + esc(t('model.description')) + '</label><textarea id="edit-model-desc" rows="2">' + esc(model.description || '') + '</textarea></div>' +
16144
+ '<div class="auto-input-group"><label>' + esc(t('model.thinkingTierMap')) + '</label><textarea id="edit-model-tier-map" rows="3">' + esc(window.serializeThinkingTierMap(model.thinking_tier_map)) + '</textarea>' +
16145
+ '<div style="margin-top:10px;font-size:11px;color:var(--text-dim);">' + esc(t('model.thinkingTierMapHelp')) + '</div></div>' +
16092
16146
  '<div style="margin-top:10px;"><button class="sec-btn primary" onclick="window.saveModelEdit(' + provIdx + ',' + modelIdx + ')">' + esc(t('common.save')) + '</button></div>');
16093
16147
  };
16094
16148
 
@@ -16101,11 +16155,13 @@ window.saveModelEdit = function(oldProvIdx, modelIdx) {
16101
16155
  var visionEl = document.getElementById('edit-model-vision');
16102
16156
  var thinkingEl = document.getElementById('edit-model-thinking');
16103
16157
  var descEl = document.getElementById('edit-model-desc');
16158
+ var tierMapEl = document.getElementById('edit-model-tier-map');
16104
16159
  var name = nameEl ? nameEl.value.trim() : '';
16105
16160
  var newProvIdx = provEl ? parseInt(provEl.value) : oldProvIdx;
16106
16161
  if (!name || isNaN(newProvIdx) || !state.providers[newProvIdx]) return;
16107
16162
  var previous = oldProvider.models[modelIdx];
16108
16163
  if (typeof previous === 'string') previous = { name: previous, display: previous };
16164
+ var tierMap = window.parseThinkingTierMap(tierMapEl ? tierMapEl.value : '');
16109
16165
  var updated = Object.assign({}, previous, {
16110
16166
  name: name,
16111
16167
  display: name,
@@ -16114,6 +16170,8 @@ window.saveModelEdit = function(oldProvIdx, modelIdx) {
16114
16170
  vision: !!(visionEl && visionEl.checked),
16115
16171
  thinking: !!(thinkingEl && thinkingEl.checked)
16116
16172
  });
16173
+ if (Object.keys(tierMap).length) updated.thinking_tier_map = tierMap;
16174
+ else delete updated.thinking_tier_map;
16117
16175
  oldProvider.models.splice(modelIdx, 1);
16118
16176
  if (!state.providers[newProvIdx].models) state.providers[newProvIdx].models = [];
16119
16177
  state.providers[newProvIdx].models.push(updated);