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.
- package/config.example.json +5 -0
- package/dist/conversation-utility-host.bundle.cjs +307 -33
- package/dist/core/agent.d.ts +52 -4
- package/dist/core/agent.js +258 -23
- package/dist/core/agentKernelRunner.js +33 -16
- package/dist/core/config.d.ts +8 -0
- package/dist/core/conversationKernel.d.ts +16 -0
- package/dist/core/conversationKernel.js +34 -1
- package/dist/core/toolPolicy.js +3 -0
- package/dist/llm/provider.d.ts +13 -1
- package/dist/llm/provider.js +42 -1
- package/dist/providers/provider-adapter.d.ts +3 -1
- package/dist/tools/index.js +3 -1
- package/dist/tui/src/render.js +54 -30
- package/dist/tui/src/state.js +3 -0
- package/dist/ui/index.html +67 -9
- package/dist/wsl-agent-host.bundle.cjs +307 -33
- package/package.json +5 -3
|
@@ -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
|
-
|
|
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) {
|
package/dist/core/toolPolicy.js
CHANGED
|
@@ -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',
|
package/dist/llm/provider.d.ts
CHANGED
|
@@ -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;
|
package/dist/llm/provider.js
CHANGED
|
@@ -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
|
-
|
|
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 语义的上下文缓存
|
package/dist/tools/index.js
CHANGED
|
@@ -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('
|
|
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 } }, []),
|
package/dist/tui/src/render.js
CHANGED
|
@@ -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
|
-
...
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
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
|
-
...(
|
|
1150
|
-
const
|
|
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
|
-
...(
|
|
1164
|
-
?
|
|
1165
|
-
const
|
|
1166
|
-
|
|
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
|
"",
|
package/dist/tui/src/state.js
CHANGED
|
@@ -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,
|
package/dist/ui/index.html
CHANGED
|
@@ -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': '
|
|
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': '
|
|
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"
|
|
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 balanced=medium 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
|
-
|
|
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);
|