mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 06:15:58 -10:00
Functional-test fixes: agent tok/s and context, plain tool rows, Thinking Off proof, profile used, no download wording for installed models, plain chat, smaller agent prompt
- agent path: live stats frames and a final tok/s from the stream timing; the context is the last request (one source) - tool rows say what happened in words (Wrote a file, Ran a command, Couldn't write the file) with the file or command; approval cards show it too - a launch that returned without error reports 'Started.' instead of 'nothing to show' - agent system prompt replaced by a short plain one; goal, skill, workflow and fork tools left out (first message about 7.5k -> about 4.3k tokens) - Models: the running model reports the profile it was started with and the page shows it; Reasoning popover names the loaded native model - installed model with other settings: 'Apply settings and load', no size, no download wording - plain chat: agent:false or tools:false answers directly and never runs a tool - tests: stats/context/verbs/launch/prompt size, server-side enable_thinking=false with the agent's request shape, plain chat over HTTP, ui-flows profile scenario
This commit is contained in:
1 parent
76cdc271c0
commit
394de92915
26 files changed
+764
-39
No files matched your search
@@ -30,7 +30,7 @@ const nodeExe = resolve(args.node ?? join(staged, 'node', process.platform === '
|
||||
const work = mkdtempSync(join(tmpdir(), 'strata-ui-flows-'));
|
||||
const shots = resolve(args.shots ?? join(work, 'shots'));
|
||||
mkdirSync(shots, { recursive: true });
|
||||
const only = new Set(String(args.only ?? 'download,models,tune,fail,delete,theme,restart,perf,polish,stop,latest').split(','));
|
||||
const only = new Set(String(args.only ?? 'download,models,profile,tune,fail,delete,theme,restart,perf,polish,stop,latest').split(','));
|
||||
const isWin = process.platform === 'win32';
|
||||
const { chromium } = await import(pathToFileURL(join(repoHost, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
|
||||
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
|
||||
@@ -266,6 +266,59 @@ await scenario('models', { installed: true }, async ({ base, api, browser, shot
|
||||
check(page.problems.length === 0, 'no page error', page.problems.join('; '));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ the profile used, the popover, the pop-up
|
||||
await scenario('profile', { installed: true }, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.getByText(/Conversation context/i).first().waitFor({ timeout: 30000 });
|
||||
await delay(1500);
|
||||
const work = () => page.getByRole('button', { name: /Leave 20% for my work/ }).first();
|
||||
const full = () => page.getByRole('button', { name: /Full performance/ }).first();
|
||||
await work().click();
|
||||
check(await work().getAttribute('aria-pressed') === 'true', 'Leave 20% is chosen before loading');
|
||||
await page.getByRole('button', { name: /^Load model$/ }).first().click();
|
||||
let ready = false;
|
||||
for (let i = 0; i < 90 && !ready; i++) { const s = (await api('/api/native')).body; ready = s.instances.some(x => x.status === 'ready'); await delay(500); }
|
||||
check(ready, 'the model loads');
|
||||
const inst = (await api('/api/native')).body.instances.find(x => x.status === 'ready');
|
||||
check(inst?.resourceProfile === 'work', 'the running model reports the profile it was started with', String(inst?.resourceProfile));
|
||||
await delay(1500);
|
||||
check(await work().getAttribute('aria-pressed') === 'true' && await full().getAttribute('aria-pressed') === 'false', 'after the load the page still shows Leave 20%, not Full performance');
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.getByRole('button', { name: /^Unload$/ }).first().waitFor({ timeout: 30000 });
|
||||
await delay(1000);
|
||||
check(await work().getAttribute('aria-pressed') === 'true', 'and after a reload it is still the profile in use');
|
||||
await shot(page, '1-profile-used');
|
||||
// the reasoning popover names the loaded model
|
||||
await page.evaluate(() => { location.hash = '#/chat'; });
|
||||
const effort = page.getByRole('button', { name: /^Reasoning effort/ }).first();
|
||||
await effort.waitFor({ timeout: 30000 });
|
||||
await effort.click();
|
||||
const header = await page.locator('.reasoning-menu-model strong').first().textContent({ timeout: 10000 });
|
||||
check(header && !/No model loaded/i.test(header), 'the Reasoning popover names the loaded model, not "No model loaded"', String(header));
|
||||
await shot(page, '2-reasoning-popover');
|
||||
await page.keyboard.press('Escape');
|
||||
// an installed model asked for with other settings: the pop-up never speaks of downloading
|
||||
await page.evaluate(() => { location.hash = '#/models'; });
|
||||
await page.getByRole('button', { name: /^Unload$/ }).first().waitFor({ timeout: 30000 });
|
||||
await page.getByRole('button', { name: /^Unload$/ }).first().click();
|
||||
await page.getByRole('button', { name: /^Load model$/ }).first().waitFor({ timeout: 30000 });
|
||||
await page.getByRole('combobox', { name: 'Conversation context' }).first().click();
|
||||
const options = page.getByRole('option');
|
||||
const pick = (await options.allTextContents()).findIndex(t => /^8K$/.test(t.trim()));
|
||||
if (pick >= 0) await options.nth(pick).click();
|
||||
await page.getByRole('button', { name: /^Load model$|^Checking/ }).first().click();
|
||||
const dialog = page.getByRole('dialog').first();
|
||||
await dialog.waitFor({ timeout: 15000 }).catch(() => {});
|
||||
const text = (await dialog.textContent().catch(() => '')) ?? '';
|
||||
await shot(page, '3-apply-dialog');
|
||||
if (text) {
|
||||
check(!/Download and prepare|Download . load|approximately|[0-9] GB/i.test(text), 'for a downloaded model the pop-up has no download wording or size', text.replace(/\s+/g, ' ').slice(0, 600));
|
||||
check(/Apply/.test(text), 'it says what happens: apply settings and load');
|
||||
} else console.log('info the settings were the saved ones, so loading needed no pop-up at all');
|
||||
check(page.problems.length === 0, 'no page error', page.problems.join('; '));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ tune + reading size
|
||||
await scenario('tune', { installed: true, env: { FAKE_TOKEN_DELAY: '0.02', FAKE_REPLY: ['I am thinking about it.', '</think>', '', 'Here is a list:', '', '- one', '- two', '', '## Heading', '', 'Some `code` and **bold** text.', '', '| a | b |', '|---|---|', '| 1 | 2 |', '', '```', 'code line', '```', ''].join(String.fromCharCode(10)) } }, async ({ base, api, browser, dirs, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
@@ -363,7 +416,7 @@ await scenario('delete', { installed: true }, async ({ base, api, browser, dirs,
|
||||
await delay(600);
|
||||
await shot(page, '1-delete-confirm');
|
||||
const dialog = await page.getByRole('dialog').last().innerText();
|
||||
check(/Delete this version\?/.test(dialog) && /Remove 83\.6 GB of downloaded files\. You can download this version again\./.test(dialog), 'the confirm dialog uses the existing words and the real size', dialog.replace(/\s+/g, ' ').slice(0, 200));
|
||||
check(/Delete this version\?/.test(dialog) && /Remove 83\.6 GB of downloaded files\. You can download this version again\./.test(dialog), 'the confirm dialog uses the existing words and the real size', dialog.replace(/\s+/g, ' ').slice(0, 600));
|
||||
check(/running model will be unloaded/i.test(dialog), 'and says the loaded model is unloaded first');
|
||||
await page.getByRole('button', { name: /^Delete version$/ }).click();
|
||||
let state = null;
|
||||
|
||||
@@ -59,6 +59,35 @@ const WORDS = {
|
||||
lsp: a => ['Looks up how the code fits together.', a.file_path],
|
||||
};
|
||||
|
||||
/**
|
||||
* What a card says it is doing, in the person's words: [while it runs, when it worked, when it failed]. The UI translates
|
||||
* these fixed sentences and writes the target (a file path, the command) beside them, so a row never shows `pwsh · Ran`.
|
||||
*/
|
||||
const VERBS = {
|
||||
write: ['Writing a file', 'Wrote a file', "Couldn't write the file"],
|
||||
edit: ['Changing a file', 'Changed a file', "Couldn't change the file"],
|
||||
read: ['Reading a file', 'Read a file', "Couldn't read the file"],
|
||||
read_image: ['Looking at a picture', 'Looked at a picture', "Couldn't look at the picture"],
|
||||
glob: ['Searching for files', 'Searched for files', "Couldn't search for files"],
|
||||
grep: ['Searching inside files', 'Searched inside files', "Couldn't search inside files"],
|
||||
pwsh: ['Running a command', 'Ran a command', 'The command failed'],
|
||||
bash: ['Running a command', 'Ran a command', 'The command failed'],
|
||||
web_fetch: ['Opening a web page', 'Opened a web page', "Couldn't open the web page"],
|
||||
web_search: ['Searching the web', 'Searched the web', "Couldn't search the web"],
|
||||
todo_write: ['Updating its to-do list', 'Updated its to-do list', "Couldn't update its to-do list"],
|
||||
present: ['Showing you a result', 'Showed you a result', "Couldn't show the result"],
|
||||
working_directory: ['Checking the folder', 'Checked the folder', "Couldn't check the folder"],
|
||||
job_list: ['Checking background tasks', 'Checked background tasks', "Couldn't check background tasks"],
|
||||
job_output: ['Reading a background task', 'Read a background task', "Couldn't read the background task"],
|
||||
job_kill: ['Stopping a background task', 'Stopped a background task', "Couldn't stop the background task"],
|
||||
};
|
||||
const GENERIC_VERBS = ['Using a tool', 'Used a tool', 'The tool failed'];
|
||||
|
||||
/** The sentence for a card in this state (the first for a running or waiting card). */
|
||||
export function toolVerb(name, state) {
|
||||
const v = VERBS[name] ?? GENERIC_VERBS;
|
||||
return state === 'error' ? v[2] : state === 'done' ? v[1] : v[0];
|
||||
}
|
||||
/** The ToolCall fields the card needs, from one Harness tool call. */
|
||||
export function presentTool({ callId, name, args, reason = null }) {
|
||||
const scope = toolScope(name);
|
||||
@@ -69,7 +98,7 @@ export function presentTool({ callId, name, args, reason = null }) {
|
||||
const effect = reason ?? (words ? words[0] : scope === 'shell' ? (args.description ?? 'Runs a command in the workspace.')
|
||||
: scope === 'write' ? 'Changes a file in the workspace.'
|
||||
: scope === 'read' ? 'Reads from the workspace.' : 'Uses a connected tool.');
|
||||
return { id: callId, name, args, preview: clip(preview, 600), effect: clip(effect, 400), scope };
|
||||
return { id: callId, name, args, preview: clip(preview, 600), effect: clip(effect, 400), scope, verb: toolVerb(name, 'running'), target: clip(name in VERBS ? preview : name, 300) };
|
||||
}
|
||||
|
||||
/** A tool's failure in plain words, or null when there is nothing better than the original text. */
|
||||
@@ -91,18 +120,24 @@ export function withPlain(raw) {
|
||||
return plain ? `${plain}\n\nDetails: ${raw.replace(/^Error:\s*/, '')}` : raw;
|
||||
}
|
||||
|
||||
/** A command whose whole point is to open something: it has no output to show when it worked. */
|
||||
const LAUNCH = /(^|[\s;|&(])(start-process|start|invoke-item|ii|explorer(?:\.exe)?|xdg-open|open|notepad(?:\.exe)?|code)(?=[\s;|&)]|$)/i;
|
||||
|
||||
/**
|
||||
* A command that succeeded and printed nothing says so in words (the card used to show "(no output)" under a "Ran" that could not
|
||||
* tell the person whether anything happened). Opening a folder or a window is not something a command can prove it did, so the
|
||||
* sentence says only what is known.
|
||||
*/
|
||||
export function outcomeText(tool, text) {
|
||||
export function outcomeText(tool, text, args = {}) {
|
||||
if (tool === 'pwsh' || tool === 'bash') {
|
||||
// In the read-only modes PowerShell runs in Constrained Language Mode and complains about the Harness's own output-encoding line:
|
||||
// noise appended after a command that worked. It says nothing about the command, so it is not shown.
|
||||
text = text.replace(/\r?\n?\[stderr\]\r?\n(?:Cannot create type\. Only core types are supported in this language mode\.[\s\S]*?CannotCreateTypeConstrainedLanguage\s*)+$/, '');
|
||||
if (!text.trim()) text = '(no output)';
|
||||
}
|
||||
// Starting a program or opening a folder prints nothing when it worked. The model used to read "finished, nothing to show" as "cannot tell
|
||||
// whether it opened" and told the person so; a launch that returned without an error did start, and the result says that plainly.
|
||||
if ((tool === 'pwsh' || tool === 'bash') && /^\(no output\)$/.test(text.trim()) && LAUNCH.test(String(args?.command ?? ''))) return 'Started. The program, folder or file was opened.';
|
||||
if ((tool === 'pwsh' || tool === 'bash') && /^\(no output\)$/.test(text.trim())) return 'The command finished without any error. It printed nothing, so there is nothing more to show.';
|
||||
return text;
|
||||
}
|
||||
@@ -131,8 +166,11 @@ export class TurnProjector {
|
||||
this.tools = new Map(); // callId -> { card, calledAt }
|
||||
this.tokensIn = 0;
|
||||
this.tokensOut = 0;
|
||||
this.contextTokens = null; // the LAST model request's prompt + reply: what the context holds now (the sums above count every request again)
|
||||
this.usageSeen = false;
|
||||
this.firstTokenAt = null;
|
||||
this.streams = new Map(); // attemptId -> { first, last, chunks, chars }: the time a model request actually spent producing
|
||||
this.lastStats = 0;
|
||||
this.emittedText = new Map(); // attemptId -> chars sent live
|
||||
this.ended = null; // the turn/end reason, once seen
|
||||
this.denied = new Set(); // callIds a human or the policy rejected
|
||||
@@ -143,9 +181,31 @@ export class TurnProjector {
|
||||
const entry = this.tools.get(callId);
|
||||
if (!entry) return null;
|
||||
entry.card = { ...entry.card, ...patch };
|
||||
entry.card.verb = toolVerb(entry.card.name, entry.card.state);
|
||||
return { type: 'agent-tool', tool: entry.card };
|
||||
}
|
||||
|
||||
/* The speed shown while a reply streams, and after it: everything the model generated (reasoning included) over the time its
|
||||
requests spent producing it, not over the whole turn (tool runs and the wait for the prompt are not decoding). One stream per model
|
||||
request; tokens are one per streamed chunk, or characters / 4 when a server batches. */
|
||||
#produced(attemptId, text, out) {
|
||||
const now = Date.now();
|
||||
const s = this.streams.get(attemptId) ?? { first: now, last: now, chunks: 0, chars: 0 };
|
||||
s.last = now; s.chunks += 1; s.chars += text.length;
|
||||
this.streams.set(attemptId, s);
|
||||
if (now - this.lastStats < 400) return;
|
||||
const { tokens, seconds } = this.decode();
|
||||
if (seconds < 0.5) return;
|
||||
this.lastStats = now;
|
||||
out.push({ type: 'stats', tokensOut: tokens, tps: Number((tokens / seconds).toFixed(1)), source: 'stream' });
|
||||
}
|
||||
|
||||
decode() {
|
||||
let tokens = 0, ms = 0;
|
||||
for (const s of this.streams.values()) { tokens += s.chars / Math.max(1, s.chunks) < 8 ? s.chunks : Math.ceil(s.chars / 4); ms += s.last - s.first; }
|
||||
return { tokens, seconds: ms / 1000 };
|
||||
}
|
||||
|
||||
/** Process one `session/follow` item (event or assistant-stream). Returns frames. */
|
||||
consume(item) {
|
||||
const out = [];
|
||||
@@ -155,16 +215,19 @@ export class TurnProjector {
|
||||
const c = f.chunk;
|
||||
if (c.type === 'text-delta' && c.text) {
|
||||
this.firstTokenAt ??= Date.now();
|
||||
this.#produced(f.attemptId, c.text, out);
|
||||
this.emittedAny = true;
|
||||
this.emittedText.set(f.attemptId, (this.emittedText.get(f.attemptId) ?? 0) + c.text.length);
|
||||
out.push({ type: 'delta', text: c.text });
|
||||
} else if (c.type === 'reasoning-delta' && c.text) {
|
||||
this.firstTokenAt ??= Date.now();
|
||||
this.#produced(f.attemptId, c.text, out);
|
||||
out.push({ type: 'reasoning', text: c.text });
|
||||
} else if (c.type === 'usage' && c.usage) {
|
||||
this.usageSeen = true;
|
||||
this.tokensIn += Number(c.usage.inputTokens) || 0;
|
||||
this.tokensOut += Number(c.usage.outputTokens) || 0;
|
||||
this.contextTokens = (Number(c.usage.inputTokens) || 0) + (Number(c.usage.outputTokens) || 0);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
@@ -220,7 +283,7 @@ export class TurnProjector {
|
||||
const frame = this.#card(callId, {
|
||||
state: denied ? 'denied' : isError ? 'error' : 'done',
|
||||
// A card the person (or the mode) refused keeps its plain sentence; the model's own error text ("the user rejected ...") is not for them.
|
||||
result: this.byPolicy.has(callId) ? NOT_IN_MODE : denied ? (entry.card.result ?? 'Denied. Nothing ran.') : isError ? withPlain(resultText(d.message)) : outcomeText(entry.card.name, resultText(d.message)),
|
||||
result: this.byPolicy.has(callId) ? NOT_IN_MODE : denied ? (entry.card.result ?? 'Denied. Nothing ran.') : isError ? withPlain(resultText(d.message)) : outcomeText(entry.card.name, resultText(d.message), entry.card.args),
|
||||
durationMs: Math.max(0, (e.time ?? 0) - entry.calledAt),
|
||||
});
|
||||
if (frame) out.push(frame);
|
||||
@@ -256,7 +319,10 @@ export class TurnProjector {
|
||||
const usage = this.usageSeen
|
||||
? { tokensIn: this.tokensIn, tokensOut: this.tokensOut, usageSource: 'reported' }
|
||||
: { tokensIn: null, tokensOut: null, usageSource: 'unavailable' };
|
||||
const base = { durationMs, ttftMs: this.firstTokenAt ? this.firstTokenAt - this.startedAt : null, model, contextMax };
|
||||
const decoded = this.decode();
|
||||
const generated = this.usageSeen && this.tokensOut > 0 ? this.tokensOut : decoded.tokens;
|
||||
const tps = decoded.seconds >= 0.3 && generated > 0 ? Number((generated / decoded.seconds).toFixed(1)) : null;
|
||||
const base = { durationMs, ttftMs: this.firstTokenAt ? this.firstTokenAt - this.startedAt : null, model, contextMax, tps, contextTokens: this.contextTokens };
|
||||
switch (reason.kind) {
|
||||
case 'completed':
|
||||
return [{ type: 'done', ...base, ...usage, finishReason: 'stop' }];
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
* preset (nothing can change); the Harness's plan mode needs an interactive review the app does not have.
|
||||
* - `tool-bash` / `tool-pwsh`: exactly one, by operating system (the Harness chooses the same at load).
|
||||
* - `tool-plugin-manager` stays disabled, as in `standard`.
|
||||
* - Prompt size: a local model reads every tool schema and every instruction on every first message (about 9,000 tokens at
|
||||
* the start, 20 s before the first word on a 64K model). The goal tools, the skill tool (no skill ships), and the
|
||||
* workflow tool (3.4 KB of schema on its own) are left out; they are the largest and the least used. Helper agents,
|
||||
* files, shell, search, todo, web fetch and present stay.
|
||||
*/
|
||||
|
||||
/** Plugin ids of the pinned Harness `standard` preset, in its order (the drift check). */
|
||||
@@ -26,7 +30,22 @@ export const STANDARD_PLUGIN_IDS = [
|
||||
'tool-ask-user', 'tool-todo', 'tool-web', 'present', 'tool-plugin-manager',
|
||||
];
|
||||
/** What we leave out of it. */
|
||||
export const REMOVED_PLUGIN_IDS = ['tool-schedule', 'tool-ask-user', 'planning'];
|
||||
export const REMOVED_PLUGIN_IDS = ['tool-schedule', 'tool-ask-user', 'planning', 'skill-filesystem', 'tool-skill', 'command-goal', 'tool-goal'];
|
||||
/** Plugins removed from inside the `delegation` group (the group stays: helper agents are a feature). */
|
||||
export const REMOVED_DELEGATION_IDS = ['workflow-ptc', 'tool-workflow', 'tool-subagent-fork'];
|
||||
|
||||
/**
|
||||
* The whole system prompt of an app chat. The Harness's own text is about 4.8 KB (about 1,200 tokens) of guidance for working on
|
||||
* the Harness itself (its checkout, its web page, its plugins): a local model pays for it on every first message and the person
|
||||
* can see its words in the chat ("DSH", "sandbox"). This one says what a chat needs and nothing else. The tools state their own rules.
|
||||
*/
|
||||
export const AGENT_PROMPT = [
|
||||
'You are a helpful assistant that can also work on this computer through your tools. You are running as {{model}}.',
|
||||
'Answer questions directly and briefly; use a tool only when the task needs one. The person reads your replies in a chat: use plain words, not tool names or technical jargon.',
|
||||
'Look before you change: read a file before you edit or overwrite it, and prefer a small edit to a rewrite. Search with the glob and grep tools and read files with the read tool; use the shell for running programs.',
|
||||
'When you start a program or open a folder or file for the person, the command returning without an error means it worked: say plainly that it is open. Do not say you could not confirm it. If a command fails, say what went wrong in one sentence and what you will try next.',
|
||||
'Tokens prefixed with @ are files or folders the person pointed at. A failed or refused action changed nothing; say so, and do not claim work you did not do.',
|
||||
].join('\n\n');
|
||||
|
||||
/**
|
||||
* @param {{platform?: string, search?: boolean}} [options]
|
||||
@@ -35,7 +54,7 @@ export const REMOVED_PLUGIN_IDS = ['tool-schedule', 'tool-ask-user', 'planning']
|
||||
export function standardPreset({ platform = process.platform, search = false } = {}) {
|
||||
const windows = platform === 'win32';
|
||||
const plugins = [
|
||||
{ id: 'persona', name: '@deepseek-ai/dsh-persona', config: { prefix: 'You are a coding agent powered by the {{model}} model.' } },
|
||||
{ id: 'persona', name: '@deepseek-ai/dsh-persona', config: { prefix: AGENT_PROMPT, complete: true, includeRuntimeContext: false } },
|
||||
{ id: 'agent-instructions', name: '@deepseek-ai/dsh-agent-instructions', config: { maxBytes: 65536 } },
|
||||
{ id: 'time-context', name: '@deepseek-ai/dsh-time-context' },
|
||||
windows
|
||||
@@ -44,10 +63,6 @@ export function standardPreset({ platform = process.platform, search = false } =
|
||||
{ id: 'tool-fs', name: '@deepseek-ai/dsh-tool-fs' },
|
||||
{ id: 'tool-fs-search', name: '@deepseek-ai/dsh-tool-fs-search', config: { sampleOverCapGlobResults: false } },
|
||||
{ id: 'tool-jobs', name: '@deepseek-ai/dsh-tool-jobs' },
|
||||
{ id: 'skill-filesystem', name: '@deepseek-ai/dsh-skill-filesystem' },
|
||||
{ id: 'tool-skill', name: '@deepseek-ai/dsh-tool-skill' },
|
||||
{ id: 'command-goal', name: '@deepseek-ai/dsh-command-goal' },
|
||||
{ id: 'tool-goal', name: '@deepseek-ai/dsh-tool-goal' },
|
||||
{
|
||||
id: 'compaction', name: 'cordis:group', group: true, isolate: { compaction: true, toolResultPruner: true },
|
||||
config: [
|
||||
@@ -62,9 +77,6 @@ export function standardPreset({ platform = process.platform, search = false } =
|
||||
{ id: 'tool-subagent-control', name: '@deepseek-ai/dsh-tool-subagent-control' },
|
||||
{ id: 'tool-subagent-list-agents', name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' },
|
||||
{ id: 'tool-subagent', name: '@deepseek-ai/dsh-tool-subagent', config: { provider: 'spawn', toolName: 'subagent', modelSelectionSettings: true } },
|
||||
{ id: 'tool-subagent-fork', name: '@deepseek-ai/dsh-tool-subagent', config: { provider: 'fork', toolName: 'subagent_fork' } },
|
||||
{ id: 'workflow-ptc', name: '@deepseek-ai/dsh-workflow-ptc', config: { provider: 'spawn' } },
|
||||
{ id: 'tool-workflow', name: '@deepseek-ai/dsh-tool-workflow' },
|
||||
],
|
||||
},
|
||||
{ id: 'tool-todo', name: '@deepseek-ai/dsh-tool-todo', config: { allowParallelInProgress: true } },
|
||||
|
||||
@@ -1441,7 +1441,7 @@ async function api(req, res, path, method, url) {
|
||||
detaches from the turn (it keeps running in the Harness); Stop is its own request. */
|
||||
const chatId = typeof body.sessionId === 'string' && body.sessionId ? body.sessionId : null;
|
||||
const agentSkip = process.env.STRATA_AGENT === 'off' ? 'The agent is turned off (STRATA_AGENT=off).'
|
||||
: body.agent === false ? 'This chat runs without the agent.'
|
||||
: body.agent === false || body.tools === false ? 'This chat runs without the agent.'
|
||||
: !chatId ? 'This request did not name a chat.'
|
||||
: images.length ? 'The agent does not take attached pictures yet; this message was sent directly.'
|
||||
: !(body.remoteModel || native.hasLive()) ? 'The agent needs a loaded Strata model or an API model.'
|
||||
@@ -1454,7 +1454,7 @@ async function api(req, res, path, method, url) {
|
||||
return undefined;
|
||||
}
|
||||
res.write(`${JSON.stringify({ type: 'notice', code: 'agent-unavailable', message: `${capability.reason}`, ...(capability.detail ? { details: capability.detail } : {}) })}\n`);
|
||||
} else if (agentSkip && chatId && body.agent !== false && process.env.STRATA_AGENT !== 'off') {
|
||||
} else if (agentSkip && chatId && body.agent !== false && body.tools !== false && process.env.STRATA_AGENT !== 'off') {
|
||||
res.write(`${JSON.stringify({ type: 'notice', code: 'agent-skipped', message: agentSkip })}\n`);
|
||||
}
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
if(!args.multiple){if(busy(requests().filter(r=>instances.has(r.instanceId))))throw new Error('Finish or cancel native requests before switching models.');for(const id of [...instances.keys()])await unload(id);}else if(matching)await unload(matching.id);
|
||||
if(job.state!=='running')return;
|
||||
writeFileSync(configPath,JSON.stringify(cfg,null,2),{mode:0o600});
|
||||
const instance={id,apiModelId,fingerprint,context:entry.context??0,aliases:cfg.aliases??[],vision:!!cfg.vision,model:args.model,label:entry.model_name??args.model,port:cfg.port,gpus:Array.isArray(cfg.gpu)?cfg.gpu:cfg.gpu===undefined?[]:[cfg.gpu],status:'loading',phase:'loading',parallel:cfg.parallel??1,mcpServers:cfg.mcp_servers??cfg.mcpServers??{},key,configPath,loadedAt:new Date().toISOString(),metrics:null,error:null};
|
||||
const instance={id,apiModelId,fingerprint,context:entry.context??0,aliases:cfg.aliases??[],vision:!!cfg.vision,resourceProfile:cfg.ui_resource_profile??null,model:args.model,label:entry.model_name??args.model,port:cfg.port,gpus:Array.isArray(cfg.gpu)?cfg.gpu:cfg.gpu===undefined?[]:[cfg.gpu],status:'loading',phase:'loading',parallel:cfg.parallel??1,mcpServers:cfg.mcp_servers??cfg.mcpServers??{},key,configPath,loadedAt:new Date().toISOString(),metrics:null,error:null};
|
||||
let proc=null,started=null,retried=null;
|
||||
if(packaged){
|
||||
/* The engine starts under Control's owner runtime, not as our child: a saved profile per slot, then `runtime start`. */
|
||||
|
||||
@@ -163,7 +163,7 @@ export function ComposerControls({ session, onPreview, onAttach, onError, onSlas
|
||||
<button role="menuitem" className="menu__item" onClick={() => { setMenu(false); onSlash(); }}><span className="slash-command-icon" aria-hidden="true">/</span>{tr("Slash commands")}</button>
|
||||
</FloatingMenu> : null)}
|
||||
{tr(preferences ? <FloatingMenu anchor={preferences === 'mode' ? modeTrigger.current : thinkingTrigger.current} title={tr(preferences === 'mode' ? 'Mode' : 'Reasoning effort')} width={preferences === 'mode' ? 330 : 280} onClose={() => setPreferences(null)}>
|
||||
{tr(preferences === 'mode' ? <>{tr(modes.map((option, index) => <button key={option.value} role="menuitemradio" aria-checked={mode.value === option.value} className="menu__item composer-mode-option" onClick={() => { setChatPreferences(session?.id, { mode: option.value }); setPreferences(null); }}><span><strong>{tr(option.label)}</strong><small>{tr(option.hint)}</small></span>{tr(mode.value === option.value ? <Icon name="check" size={16}/> : null)}<kbd className="menu-shortcut">{tr(index + 1)}</kbd></button>))}</> : <><div className="reasoning-menu-model"><Icon name={apiModel ? 'link' : modelFamilyIcon(loaded?.family)} size={18}/><strong>{tr(apiModel?.name ?? loaded?.family ?? 'No model loaded')}</strong></div><p className="composer-menu-heading">{tr("Reasoning effort")}</p>{tr(efforts.map(option => <button key={option} role="menuitemradio" aria-checked={effort === option} className="menu__item" onClick={() => { setChatPreferences(session?.id, { thinking: option }); setPreferences(null); }}>{tr(option)}<span className="spacer"/>{tr(effort === option ? <Icon name="check" size={16}/> : null)}</button>))}<p className="composer-menu-heading">{tr("Thinking budget")}</p>{tr(THINKING_BUDGETS.map(([value, label]) => <button key={value} role="menuitemradio" aria-checked={budget === value} disabled={effort === 'Off'} className="menu__item" onClick={() => { setChatPreferences(session?.id, { thinkingBudget: value }); setPreferences(null); }}>{tr(label)}<span className="spacer"/>{tr(budget === value ? <Icon name="check" size={16}/> : null)}</button>))}</>)}
|
||||
{tr(preferences === 'mode' ? <>{tr(modes.map((option, index) => <button key={option.value} role="menuitemradio" aria-checked={mode.value === option.value} className="menu__item composer-mode-option" onClick={() => { setChatPreferences(session?.id, { mode: option.value }); setPreferences(null); }}><span><strong>{tr(option.label)}</strong><small>{tr(option.hint)}</small></span>{tr(mode.value === option.value ? <Icon name="check" size={16}/> : null)}<kbd className="menu-shortcut">{tr(index + 1)}</kbd></button>))}</> : <><div className="reasoning-menu-model"><Icon name={apiModel ? 'link' : modelFamilyIcon(loaded?.family ?? nativeModel?.label)} size={18}/><strong>{tr(apiModel?.name ?? (nativeModel && nativeModel.status === 'ready' ? nativeModel.label : null) ?? loaded?.family ?? 'No model loaded')}</strong></div><p className="composer-menu-heading">{tr("Reasoning effort")}</p>{tr(efforts.map(option => <button key={option} role="menuitemradio" aria-checked={effort === option} className="menu__item" onClick={() => { setChatPreferences(session?.id, { thinking: option }); setPreferences(null); }}>{tr(option)}<span className="spacer"/>{tr(effort === option ? <Icon name="check" size={16}/> : null)}</button>))}<p className="composer-menu-heading">{tr("Thinking budget")}</p>{tr(THINKING_BUDGETS.map(([value, label]) => <button key={value} role="menuitemradio" aria-checked={budget === value} disabled={effort === 'Off'} className="menu__item" onClick={() => { setChatPreferences(session?.id, { thinkingBudget: value }); setPreferences(null); }}>{tr(label)}<span className="spacer"/>{tr(budget === value ? <Icon name="check" size={16}/> : null)}</button>))}</>)}
|
||||
</FloatingMenu> : null)}
|
||||
|
||||
<Dialog open={urlOpen} title={tr("Open a website")} onClose={() => setUrlOpen(false)} actions={<><Btn onClick={() => setUrlOpen(false)}>{tr("Cancel")}</Btn><Btn variant="primary" disabled={!url.trim()} onClick={() => {
|
||||
|
||||
@@ -46,6 +46,9 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
setKv(installed.kv as typeof kv);
|
||||
setLowRam((installed.lowRamMode ?? 'auto') as typeof lowRam);
|
||||
} }, [installed?.model, installed?.ready, installed?.context, installed?.kv, installed?.lowRamMode]);
|
||||
// What the running model was really started with is what the page shows: after Load the highlight must still be the profile that was applied.
|
||||
useEffect(() => { if (loadedSelected?.resourceProfile)
|
||||
setResourceProfile(loadedSelected.resourceProfile); }, [loadedSelected?.id, loadedSelected?.resourceProfile]);
|
||||
const active = native?.jobs.find(j => j.state === 'running');
|
||||
const job = native?.jobs.find(j => j.id === jobId) ?? (!jobId ? native?.jobs.filter(j => ['load', 'install'].includes(j.kind)).at(-1) : undefined);
|
||||
const noFit = selectedSize?.on_this_pc === 'does not fit';
|
||||
@@ -65,7 +68,9 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
setVision(installed.visionEnabled === false || installed.images === 'off' ? 'no' : installed.visionDevice ?? installed.images ?? 'no');
|
||||
setKv(installed.kv ?? 'int8');
|
||||
setLowRam(installed.lowRamMode ?? 'auto');
|
||||
setResourceProfile(installed.resourceProfile ?? 'performance');
|
||||
// A model never tuned has no saved choice: keep the page's own (Leave 20%) rather than flipping to Full performance.
|
||||
if (installed.resourceProfile && !loadedSelected?.resourceProfile)
|
||||
setResourceProfile(installed.resourceProfile);
|
||||
} }, [installed?.model, installed?.context, installed?.kv, installed?.images, installed?.visionEnabled, installed?.visionDevice, installed?.lowRamMode, installed?.resourceProfile]);
|
||||
useEffect(() => { if (jobId && job?.state === 'done' && (job.kind === 'load' || job.kind === 'install')) {
|
||||
selectRemoteModel(null);
|
||||
@@ -111,7 +116,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
</div>
|
||||
<aside className="stack stack--lg"><section className="st-card"><h2 className="h4">{tr("This computer")}</h2><p>{tr(native?.hardware?.ram_gb || hardware?.memory.totalGb || '—')}{tr(" GB RAM")}</p>{tr(native?.hardware?.gpus.length ? native.hardware.gpus.map(g => <p className="small" key={g.index}>{tr(g.name)} · {tr(g.vram_gb)}{tr(" GB")}</p>) : <p className="small muted">{tr("No supported GPU reported by this installation.")}</p>)}<p className="small muted">{tr("Compatibility and sizes come from your installed Strata version.")}</p></section><section className="st-card"><h2 className="h4">{tr("Model files")}</h2><p className="small muted">{tr("Strata prepares all shards, the draft layer and shared files. Completed downloads are reused.")}</p><code className="native-path">{installed?.dataFolder ?? native?.downloadDirectory ?? native?.installed?.data_folder ?? 'Choose an installation'}</code><Btn size="sm" variant="ghost" onClick={() => { setSettingsSection('Storage'); go('settings'); }}>{tr("Storage settings")}</Btn></section><Btn size="sm" variant="ghost" onClick={onOtherModels}>{tr("Other local models\u2026")}</Btn></aside>
|
||||
</div>
|
||||
<Dialog open={!!plan} title={tr("Download and prepare")} compact onClose={() => !pending && setPlan(null)} actions={<><Btn disabled={pending} onClick={() => setPlan(null)}>{tr("Cancel")}</Btn><Btn variant="primary" disabled={pending || !!plan?.plan?.disk_short} onClick={() => void download()}>{tr("Download & load")}</Btn></>}><p><strong>{tr(plan?.plan?.title ?? selectedFamily?.title)}</strong>{tr(" \u00B7 approximately ")}{tr(plan?.plan?.download_gb ?? selectedSize?.download_gb)}{tr(" GB")}</p>{tr(plan?.plan?.disk_short ? <ErrorNotice message={plan.plan.disk_short}/> : null)}<p className="small muted">{tr("Existing files are checked and reused. Any currently loaded native models will be unloaded before preparing files.")}</p><p className="small muted">{tr("Your computer may feel slower while weights load. You can stop the download and resume later.")}</p>{tr(selectedSize?.images ? <p className="small muted">{tr("Vision encoder files are included. Picture reading loads only when enabled.")}</p> : null)}</Dialog>
|
||||
<Dialog open={!!plan} title={tr(installed?.ready ? "Apply settings and load" : "Download and prepare")} compact onClose={() => !pending && setPlan(null)} actions={<><Btn disabled={pending} onClick={() => setPlan(null)}>{tr("Cancel")}</Btn><Btn variant="primary" disabled={pending || !!plan?.plan?.disk_short} onClick={() => void download()}>{tr(installed?.ready ? "Apply & load" : "Download & load")}</Btn></>}><p><strong>{tr(plan?.plan?.title ?? selectedFamily?.title)}</strong>{tr(installed?.ready ? null : <>{tr(" \u00B7 approximately ")}{tr(plan?.plan?.download_gb ?? selectedSize?.download_gb)}{tr(" GB")}</>)}</p>{tr(installed?.ready ? <p className="small muted">{tr("The model files are already on this computer. Nothing will be downloaded; only the settings you chose are applied.")}</p> : null)}{tr(plan?.plan?.disk_short ? <ErrorNotice message={plan.plan.disk_short}/> : null)}{tr(installed?.ready ? <p className="small muted">{tr("Any currently loaded native models will be unloaded first.")}</p> : <><p className="small muted">{tr("Existing files are checked and reused. Any currently loaded native models will be unloaded before preparing files.")}</p><p className="small muted">{tr("Your computer may feel slower while weights load. You can stop the download and resume later.")}</p></>)}{tr(selectedSize?.images ? <p className="small muted">{tr("Vision encoder files are included. Picture reading loads only when enabled.")}</p> : null)}</Dialog>
|
||||
<NativeDeleteDialog id={deleting} onClose={() => setDeleting(null)}/>
|
||||
<Dialog open={manage} title={tr("Manage models")} onClose={() => setManage(false)} actions={<Btn onClick={() => setManage(false)}>{tr("Done")}</Btn>}><div className="native-manage">
|
||||
{tr(downloaded.length ? <div className="native-manage-list" role="list">{tr(downloaded.map(m => { const v = allSizes.find(x => x.id === m.model), loaded = native?.instances.some(i => i.model === m.model && i.status === 'ready'); return <div className="native-manage-row" role="listitem" key={m.model} data-selected={selectedSize?.id === m.model || undefined}>
|
||||
|
||||
@@ -6,7 +6,11 @@ export function chatMetrics(session:Session|null, monitor:MonitorState|null, con
|
||||
const latest=[...replies].reverse().find(m=>m.inputTokens!==undefined);
|
||||
const remote=replies.at(-1)?.remoteModel===true;
|
||||
const active=!remote&&monitor?.context?.sessionId===session?.id&&!!session;
|
||||
const used=active?monitor!.context.usedTokens:latest?latest.inputTokens!+(latest.tokens??0):null;
|
||||
// One source: what the context held after the last reply (the engine's own count for that request); the monitor's live figure only while a reply streams.
|
||||
const finished=latest?(latest.contextUsed??latest.inputTokens!+(latest.tokens??0)):null;
|
||||
const liveUsed=active&&Number.isFinite(monitor!.context.usedTokens)&&monitor!.context.usedTokens>0?monitor!.context.usedTokens:null;
|
||||
const live=replies.some(m=>m.streaming)||(active&&['reading','thinking','generating'].includes(monitor!.phase));
|
||||
const used=live?(liveUsed??finished):(finished??liveUsed);
|
||||
const max=remote?(replies.at(-1)?.contextMax||null):configuredContext??(active?monitor!.context.maxTokens:latest?.contextMax??null);
|
||||
const streaming=replies.find(m=>m.streaming);
|
||||
const speed=streaming?.tps??[...replies].reverse().find(m=>m.tps!==undefined)?.tps??null;
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface NativeSize {id:string;model:string;about:string;download_gb:num
|
||||
export interface NativeFamily {family:string;title:string;about:string;experimental:boolean;images:boolean;sizes:NativeSize[]}
|
||||
export interface NativeField {id:string;label:string;group:string;type:'number'|'boolean'|'string'|'enum'|'array';min?:number;max?:number;choices?:string[];hint?:string;supported?:boolean}
|
||||
export interface NativeJob {id:string;kind:string;guided?:boolean;model?:string;state:'running'|'done'|'error'|'cancelled';startedAt:string;summary:string;error:string|null;details?:string;result:Record<string,any>|null;progress?:Record<string,any>|null}
|
||||
export interface NativeInstance {id:string;apiModelId:string;model:string;label:string;aliases?:string[];vision?:boolean;context?:number;port:number;gpus:number[];status:'loading'|'ready'|'unloading'|'error';phase:string;loadedAt:string;error:string|null;metrics:Record<string,any>|null}
|
||||
export interface NativeInstance {id:string;apiModelId:string;model:string;label:string;aliases?:string[];vision?:boolean;resourceProfile?:'work'|'performance'|null;context?:number;port:number;gpus:number[];status:'loading'|'ready'|'unloading'|'error';phase:string;loadedAt:string;error:string|null;metrics:Record<string,any>|null}
|
||||
export interface NativeState {available:boolean;repo:string;downloadDirectory?:string;version?:string|null;summary:string;error:string|null;catalog:{families:NativeFamily[];contexts:number[];recommendation:{family:string|null;model:string|null;context?:number;why?:string};context_note:string}|null;installed:{models:{model:string;config:string;model_name:string;ready:boolean;context?:number;kv?:string;images?:string;visionEnabled?:boolean;visionDevice?:string;lowRamMode?:string;resourceProfile?:'work'|'performance';dataFolder?:string;diskGb?:number;diskBytes?:number;missing_files?:string[]}[];data_folder:string;engine:{version:string|null}}|null;hardware:{ram_gb:number;gpus:{index:number;name:string;vram_gb:number;usable:boolean}[]}|null;fields:NativeField[];instances:NativeInstance[];defaultId:string|null;jobs:NativeJob[];capabilities:{responses?:boolean;updates:boolean;rollback:boolean;calibration:boolean;componentUpdates?:boolean}}
|
||||
let state:NativeState|null=null,timer:ReturnType<typeof setInterval>|null=null,reading:Promise<void>|null=null;
|
||||
const listeners=new Set<()=>void>();
|
||||
|
||||
@@ -1332,6 +1332,8 @@ export async function sendMessage(text: string) {
|
||||
nativeModelId:nativeState()?.instances.length?nativeState()?.defaultId:undefined,
|
||||
images:await Promise.all(pictures.map(photoData)),
|
||||
useTools:state.sessions.find(s=>s.id===sessionId)?.preferences?.useTools===true,
|
||||
// A chat set to plain talk (preferences.agent === false) never runs a tool: the host answers it directly.
|
||||
...(state.sessions.find(s=>s.id===sessionId)?.preferences?.agent===false?{agent:false}:{}),
|
||||
thinking:state.sessions.find(s=>s.id===sessionId)?.preferences?.thinking??'High',
|
||||
thinkingBudget:state.sessions.find(s=>s.id===sessionId)?.preferences?.thinkingBudget??0,
|
||||
// The Mode menu's choice for this chat, read when the turn starts; the host applies it to the agent.
|
||||
@@ -1393,6 +1395,7 @@ export async function sendMessage(text: string) {
|
||||
tokens: typeof frame.tokensOut === 'number' ? frame.tokensOut : undefined,
|
||||
ttftMs: typeof frame.ttftMs === 'number' ? frame.ttftMs : undefined,
|
||||
inputTokens: typeof frame.tokensIn === 'number' ? frame.tokensIn : undefined,
|
||||
contextUsed: typeof frame.contextTokens === 'number' ? frame.contextTokens : undefined,
|
||||
usageSource: frame.usageSource === 'reported' || frame.usageSource === 'demo' ? frame.usageSource : 'unavailable',
|
||||
tps: typeof frame.tps === 'number' ? frame.tps : undefined,
|
||||
}));
|
||||
|
||||
@@ -325,6 +325,8 @@ export interface ChatMessage {
|
||||
requestId?: string;
|
||||
model?: string;
|
||||
inputTokens?: number;
|
||||
/** What the context held after this reply (the last model request's prompt + reply); an agent turn's inputTokens add every request up. */
|
||||
contextUsed?: number;
|
||||
contextMax?: number;
|
||||
usageSource?: 'reported' | 'demo' | 'unavailable' | 'estimated';
|
||||
skillsUsed?: string[];
|
||||
@@ -352,6 +354,10 @@ export interface ToolCall {
|
||||
preview: string;
|
||||
effect: string;
|
||||
scope: 'read' | 'write' | 'shell' | 'connected';
|
||||
/** The card's headline in plain words for its current state ("Wrote a file"); the host sends it, the UI translates it. */
|
||||
verb?: string;
|
||||
/** What the action is on: a file path, the command, a web address. */
|
||||
target?: string;
|
||||
state: ToolState;
|
||||
result?: string;
|
||||
durationMs?: number;
|
||||
@@ -388,7 +394,7 @@ export interface Session {
|
||||
previewOpen?: string;
|
||||
closedPreviews?: string[];
|
||||
showcaseAgentStates?: boolean;
|
||||
preferences?: { access?: 'read' | 'write' | 'full'; thinking?: string; thinkingBudget?: number; mode?: ChatMode; useTools?:boolean; /** This chat's API model; absent or null means the local model. */ remoteModel?: RemoteModelSelection | null };
|
||||
preferences?: { access?: 'read' | 'write' | 'full'; thinking?: string; thinkingBudget?: number; mode?: ChatMode; useTools?:boolean; /** false: plain conversation, no tools of any kind. */ agent?: boolean; /** This chat's API model; absent or null means the local model. */ remoteModel?: RemoteModelSelection | null };
|
||||
/** A branch does not have retained per-turn input usage. */
|
||||
statsAvailable?: boolean;
|
||||
id: string;
|
||||
|
||||
@@ -2619,5 +2619,57 @@
|
||||
"s2618": "Darf Änderungen vornehmen",
|
||||
"s2619": "Aufgabe abgeschlossen",
|
||||
"s2620": "Aufgabe braucht Ihr Ja",
|
||||
"s2621": "Aufgabe fehlgeschlagen"
|
||||
"s2621": "Aufgabe fehlgeschlagen",
|
||||
"s2622": "Datei wird geschrieben",
|
||||
"s2623": "Datei geschrieben",
|
||||
"s2624": "Die Datei konnte nicht geschrieben werden",
|
||||
"s2625": "Datei wird geändert",
|
||||
"s2626": "Datei geändert",
|
||||
"s2627": "Die Datei konnte nicht geändert werden",
|
||||
"s2628": "Datei wird gelesen",
|
||||
"s2629": "Datei gelesen",
|
||||
"s2630": "Die Datei konnte nicht gelesen werden",
|
||||
"s2631": "Bild wird angesehen",
|
||||
"s2632": "Bild angesehen",
|
||||
"s2633": "Das Bild konnte nicht angesehen werden",
|
||||
"s2634": "Dateien werden gesucht",
|
||||
"s2635": "Dateien gesucht",
|
||||
"s2636": "Die Dateisuche ist fehlgeschlagen",
|
||||
"s2637": "Dateiinhalte werden durchsucht",
|
||||
"s2638": "Dateiinhalte durchsucht",
|
||||
"s2639": "Die Suche in Dateien ist fehlgeschlagen",
|
||||
"s2640": "Befehl wird ausgeführt",
|
||||
"s2641": "Befehl ausgeführt",
|
||||
"s2642": "Der Befehl ist fehlgeschlagen",
|
||||
"s2643": "Webseite wird geöffnet",
|
||||
"s2644": "Webseite geöffnet",
|
||||
"s2645": "Die Webseite konnte nicht geöffnet werden",
|
||||
"s2646": "Das Web wird durchsucht",
|
||||
"s2647": "Das Web durchsucht",
|
||||
"s2648": "Die Websuche ist fehlgeschlagen",
|
||||
"s2649": "Aufgabenliste wird aktualisiert",
|
||||
"s2650": "Aufgabenliste aktualisiert",
|
||||
"s2651": "Die Aufgabenliste konnte nicht aktualisiert werden",
|
||||
"s2652": "Ergebnis wird angezeigt",
|
||||
"s2653": "Ergebnis angezeigt",
|
||||
"s2654": "Das Ergebnis konnte nicht angezeigt werden",
|
||||
"s2655": "Ordner wird geprüft",
|
||||
"s2656": "Ordner geprüft",
|
||||
"s2657": "Der Ordner konnte nicht geprüft werden",
|
||||
"s2658": "Hintergrundaufgaben werden geprüft",
|
||||
"s2659": "Hintergrundaufgaben geprüft",
|
||||
"s2660": "Hintergrundaufgaben konnten nicht geprüft werden",
|
||||
"s2661": "Hintergrundaufgabe wird gelesen",
|
||||
"s2662": "Hintergrundaufgabe gelesen",
|
||||
"s2663": "Die Hintergrundaufgabe konnte nicht gelesen werden",
|
||||
"s2664": "Hintergrundaufgabe wird gestoppt",
|
||||
"s2665": "Hintergrundaufgabe gestoppt",
|
||||
"s2666": "Die Hintergrundaufgabe konnte nicht gestoppt werden",
|
||||
"s2667": "Werkzeug wird verwendet",
|
||||
"s2668": "Werkzeug verwendet",
|
||||
"s2669": "Das Werkzeug ist fehlgeschlagen",
|
||||
"s2670": "Einstellungen anwenden und laden",
|
||||
"s2671": "Anwenden & laden",
|
||||
"s2672": "Die Modelldateien sind bereits auf diesem Computer. Es wird nichts heruntergeladen, nur Ihre gewählten Einstellungen werden angewendet.",
|
||||
"s2673": "Aktuell geladene native Modelle werden zuerst entladen."
|
||||
}
|
||||
@@ -2619,5 +2619,57 @@
|
||||
"s2618": "Allowed to make changes",
|
||||
"s2619": "Task finished",
|
||||
"s2620": "Task needs your yes",
|
||||
"s2621": "Task failed"
|
||||
"s2621": "Task failed",
|
||||
"s2622": "Writing a file",
|
||||
"s2623": "Wrote a file",
|
||||
"s2624": "Couldn't write the file",
|
||||
"s2625": "Changing a file",
|
||||
"s2626": "Changed a file",
|
||||
"s2627": "Couldn't change the file",
|
||||
"s2628": "Reading a file",
|
||||
"s2629": "Read a file",
|
||||
"s2630": "Couldn't read the file",
|
||||
"s2631": "Looking at a picture",
|
||||
"s2632": "Looked at a picture",
|
||||
"s2633": "Couldn't look at the picture",
|
||||
"s2634": "Searching for files",
|
||||
"s2635": "Searched for files",
|
||||
"s2636": "Couldn't search for files",
|
||||
"s2637": "Searching inside files",
|
||||
"s2638": "Searched inside files",
|
||||
"s2639": "Couldn't search inside files",
|
||||
"s2640": "Running a command",
|
||||
"s2641": "Ran a command",
|
||||
"s2642": "The command failed",
|
||||
"s2643": "Opening a web page",
|
||||
"s2644": "Opened a web page",
|
||||
"s2645": "Couldn't open the web page",
|
||||
"s2646": "Searching the web",
|
||||
"s2647": "Searched the web",
|
||||
"s2648": "Couldn't search the web",
|
||||
"s2649": "Updating its to-do list",
|
||||
"s2650": "Updated its to-do list",
|
||||
"s2651": "Couldn't update its to-do list",
|
||||
"s2652": "Showing you a result",
|
||||
"s2653": "Showed you a result",
|
||||
"s2654": "Couldn't show the result",
|
||||
"s2655": "Checking the folder",
|
||||
"s2656": "Checked the folder",
|
||||
"s2657": "Couldn't check the folder",
|
||||
"s2658": "Checking background tasks",
|
||||
"s2659": "Checked background tasks",
|
||||
"s2660": "Couldn't check background tasks",
|
||||
"s2661": "Reading a background task",
|
||||
"s2662": "Read a background task",
|
||||
"s2663": "Couldn't read the background task",
|
||||
"s2664": "Stopping a background task",
|
||||
"s2665": "Stopped a background task",
|
||||
"s2666": "Couldn't stop the background task",
|
||||
"s2667": "Using a tool",
|
||||
"s2668": "Used a tool",
|
||||
"s2669": "The tool failed",
|
||||
"s2670": "Apply settings and load",
|
||||
"s2671": "Apply & load",
|
||||
"s2672": "The model files are already on this computer. Nothing will be downloaded; only the settings you chose are applied.",
|
||||
"s2673": "Any currently loaded native models will be unloaded first."
|
||||
}
|
||||
@@ -2619,5 +2619,57 @@
|
||||
"s2618": "Puede hacer cambios",
|
||||
"s2619": "Tarea terminada",
|
||||
"s2620": "La tarea necesita tu visto bueno",
|
||||
"s2621": "La tarea falló"
|
||||
"s2621": "La tarea falló",
|
||||
"s2622": "Escribiendo un archivo",
|
||||
"s2623": "Archivo escrito",
|
||||
"s2624": "No se pudo escribir el archivo",
|
||||
"s2625": "Modificando un archivo",
|
||||
"s2626": "Archivo modificado",
|
||||
"s2627": "No se pudo modificar el archivo",
|
||||
"s2628": "Leyendo un archivo",
|
||||
"s2629": "Archivo leído",
|
||||
"s2630": "No se pudo leer el archivo",
|
||||
"s2631": "Mirando una imagen",
|
||||
"s2632": "Imagen vista",
|
||||
"s2633": "No se pudo ver la imagen",
|
||||
"s2634": "Buscando archivos",
|
||||
"s2635": "Archivos buscados",
|
||||
"s2636": "No se pudo buscar archivos",
|
||||
"s2637": "Buscando dentro de archivos",
|
||||
"s2638": "Búsqueda dentro de archivos hecha",
|
||||
"s2639": "No se pudo buscar dentro de archivos",
|
||||
"s2640": "Ejecutando un comando",
|
||||
"s2641": "Comando ejecutado",
|
||||
"s2642": "El comando falló",
|
||||
"s2643": "Abriendo una página web",
|
||||
"s2644": "Página web abierta",
|
||||
"s2645": "No se pudo abrir la página web",
|
||||
"s2646": "Buscando en la web",
|
||||
"s2647": "Búsqueda en la web hecha",
|
||||
"s2648": "No se pudo buscar en la web",
|
||||
"s2649": "Actualizando su lista de tareas",
|
||||
"s2650": "Lista de tareas actualizada",
|
||||
"s2651": "No se pudo actualizar la lista de tareas",
|
||||
"s2652": "Mostrándote un resultado",
|
||||
"s2653": "Resultado mostrado",
|
||||
"s2654": "No se pudo mostrar el resultado",
|
||||
"s2655": "Comprobando la carpeta",
|
||||
"s2656": "Carpeta comprobada",
|
||||
"s2657": "No se pudo comprobar la carpeta",
|
||||
"s2658": "Comprobando tareas en segundo plano",
|
||||
"s2659": "Tareas en segundo plano comprobadas",
|
||||
"s2660": "No se pudieron comprobar las tareas en segundo plano",
|
||||
"s2661": "Leyendo una tarea en segundo plano",
|
||||
"s2662": "Tarea en segundo plano leída",
|
||||
"s2663": "No se pudo leer la tarea en segundo plano",
|
||||
"s2664": "Deteniendo una tarea en segundo plano",
|
||||
"s2665": "Tarea en segundo plano detenida",
|
||||
"s2666": "No se pudo detener la tarea en segundo plano",
|
||||
"s2667": "Usando una herramienta",
|
||||
"s2668": "Herramienta usada",
|
||||
"s2669": "La herramienta falló",
|
||||
"s2670": "Aplicar ajustes y cargar",
|
||||
"s2671": "Aplicar y cargar",
|
||||
"s2672": "Los archivos del modelo ya están en este ordenador. No se descargará nada; solo se aplican los ajustes que elegiste.",
|
||||
"s2673": "Los modelos nativos cargados actualmente se descargarán primero."
|
||||
}
|
||||
@@ -2619,5 +2619,57 @@
|
||||
"s2618": "変更を許可する",
|
||||
"s2619": "タスクが完了しました",
|
||||
"s2620": "タスクに許可が必要です",
|
||||
"s2621": "タスクが失敗しました"
|
||||
"s2621": "タスクが失敗しました",
|
||||
"s2622": "ファイルを書き込み中",
|
||||
"s2623": "ファイルを書き込みました",
|
||||
"s2624": "ファイルを書き込めませんでした",
|
||||
"s2625": "ファイルを変更中",
|
||||
"s2626": "ファイルを変更しました",
|
||||
"s2627": "ファイルを変更できませんでした",
|
||||
"s2628": "ファイルを読み込み中",
|
||||
"s2629": "ファイルを読み込みました",
|
||||
"s2630": "ファイルを読み込めませんでした",
|
||||
"s2631": "画像を確認中",
|
||||
"s2632": "画像を確認しました",
|
||||
"s2633": "画像を確認できませんでした",
|
||||
"s2634": "ファイルを検索中",
|
||||
"s2635": "ファイルを検索しました",
|
||||
"s2636": "ファイルを検索できませんでした",
|
||||
"s2637": "ファイルの中身を検索中",
|
||||
"s2638": "ファイルの中身を検索しました",
|
||||
"s2639": "ファイルの中身を検索できませんでした",
|
||||
"s2640": "コマンドを実行中",
|
||||
"s2641": "コマンドを実行しました",
|
||||
"s2642": "コマンドが失敗しました",
|
||||
"s2643": "Webページを開いています",
|
||||
"s2644": "Webページを開きました",
|
||||
"s2645": "Webページを開けませんでした",
|
||||
"s2646": "Webを検索中",
|
||||
"s2647": "Webを検索しました",
|
||||
"s2648": "Webを検索できませんでした",
|
||||
"s2649": "やることリストを更新中",
|
||||
"s2650": "やることリストを更新しました",
|
||||
"s2651": "やることリストを更新できませんでした",
|
||||
"s2652": "結果を表示中",
|
||||
"s2653": "結果を表示しました",
|
||||
"s2654": "結果を表示できませんでした",
|
||||
"s2655": "フォルダーを確認中",
|
||||
"s2656": "フォルダーを確認しました",
|
||||
"s2657": "フォルダーを確認できませんでした",
|
||||
"s2658": "バックグラウンドのタスクを確認中",
|
||||
"s2659": "バックグラウンドのタスクを確認しました",
|
||||
"s2660": "バックグラウンドのタスクを確認できませんでした",
|
||||
"s2661": "バックグラウンドのタスクを読み込み中",
|
||||
"s2662": "バックグラウンドのタスクを読み込みました",
|
||||
"s2663": "バックグラウンドのタスクを読み込めませんでした",
|
||||
"s2664": "バックグラウンドのタスクを停止中",
|
||||
"s2665": "バックグラウンドのタスクを停止しました",
|
||||
"s2666": "バックグラウンドのタスクを停止できませんでした",
|
||||
"s2667": "ツールを使用中",
|
||||
"s2668": "ツールを使用しました",
|
||||
"s2669": "ツールが失敗しました",
|
||||
"s2670": "設定を適用して読み込む",
|
||||
"s2671": "適用して読み込む",
|
||||
"s2672": "モデルのファイルはこのコンピューターにすでにあります。ダウンロードは行われず、選んだ設定だけが適用されます。",
|
||||
"s2673": "現在読み込まれているネイティブモデルは先にアンロードされます。"
|
||||
}
|
||||
@@ -2619,5 +2619,57 @@
|
||||
"s2618": "변경 허용",
|
||||
"s2619": "작업 완료",
|
||||
"s2620": "작업에 승인이 필요합니다",
|
||||
"s2621": "작업 실패"
|
||||
"s2621": "작업 실패",
|
||||
"s2622": "파일을 쓰는 중",
|
||||
"s2623": "파일을 썼습니다",
|
||||
"s2624": "파일을 쓸 수 없었습니다",
|
||||
"s2625": "파일을 수정하는 중",
|
||||
"s2626": "파일을 수정했습니다",
|
||||
"s2627": "파일을 수정할 수 없었습니다",
|
||||
"s2628": "파일을 읽는 중",
|
||||
"s2629": "파일을 읽었습니다",
|
||||
"s2630": "파일을 읽을 수 없었습니다",
|
||||
"s2631": "그림을 보는 중",
|
||||
"s2632": "그림을 확인했습니다",
|
||||
"s2633": "그림을 볼 수 없었습니다",
|
||||
"s2634": "파일을 찾는 중",
|
||||
"s2635": "파일을 찾았습니다",
|
||||
"s2636": "파일을 찾을 수 없었습니다",
|
||||
"s2637": "파일 내용을 검색하는 중",
|
||||
"s2638": "파일 내용을 검색했습니다",
|
||||
"s2639": "파일 내용을 검색할 수 없었습니다",
|
||||
"s2640": "명령을 실행하는 중",
|
||||
"s2641": "명령을 실행했습니다",
|
||||
"s2642": "명령이 실패했습니다",
|
||||
"s2643": "웹 페이지를 여는 중",
|
||||
"s2644": "웹 페이지를 열었습니다",
|
||||
"s2645": "웹 페이지를 열 수 없었습니다",
|
||||
"s2646": "웹을 검색하는 중",
|
||||
"s2647": "웹을 검색했습니다",
|
||||
"s2648": "웹을 검색할 수 없었습니다",
|
||||
"s2649": "할 일 목록을 업데이트하는 중",
|
||||
"s2650": "할 일 목록을 업데이트했습니다",
|
||||
"s2651": "할 일 목록을 업데이트할 수 없었습니다",
|
||||
"s2652": "결과를 보여 주는 중",
|
||||
"s2653": "결과를 보여 주었습니다",
|
||||
"s2654": "결과를 보여 줄 수 없었습니다",
|
||||
"s2655": "폴더를 확인하는 중",
|
||||
"s2656": "폴더를 확인했습니다",
|
||||
"s2657": "폴더를 확인할 수 없었습니다",
|
||||
"s2658": "백그라운드 작업을 확인하는 중",
|
||||
"s2659": "백그라운드 작업을 확인했습니다",
|
||||
"s2660": "백그라운드 작업을 확인할 수 없었습니다",
|
||||
"s2661": "백그라운드 작업을 읽는 중",
|
||||
"s2662": "백그라운드 작업을 읽었습니다",
|
||||
"s2663": "백그라운드 작업을 읽을 수 없었습니다",
|
||||
"s2664": "백그라운드 작업을 중지하는 중",
|
||||
"s2665": "백그라운드 작업을 중지했습니다",
|
||||
"s2666": "백그라운드 작업을 중지할 수 없었습니다",
|
||||
"s2667": "도구를 사용하는 중",
|
||||
"s2668": "도구를 사용했습니다",
|
||||
"s2669": "도구가 실패했습니다",
|
||||
"s2670": "설정 적용 후 불러오기",
|
||||
"s2671": "적용 후 불러오기",
|
||||
"s2672": "모델 파일이 이미 이 컴퓨터에 있습니다. 아무것도 다운로드하지 않으며 선택한 설정만 적용됩니다.",
|
||||
"s2673": "현재 불러온 네이티브 모델은 먼저 언로드됩니다."
|
||||
}
|
||||
@@ -2619,5 +2619,57 @@
|
||||
"s2618": "Pode fazer alterações",
|
||||
"s2619": "Tarefa concluída",
|
||||
"s2620": "A tarefa precisa do seu sim",
|
||||
"s2621": "A tarefa falhou"
|
||||
"s2621": "A tarefa falhou",
|
||||
"s2622": "Escrevendo um arquivo",
|
||||
"s2623": "Arquivo escrito",
|
||||
"s2624": "Não foi possível escrever o arquivo",
|
||||
"s2625": "Alterando um arquivo",
|
||||
"s2626": "Arquivo alterado",
|
||||
"s2627": "Não foi possível alterar o arquivo",
|
||||
"s2628": "Lendo um arquivo",
|
||||
"s2629": "Arquivo lido",
|
||||
"s2630": "Não foi possível ler o arquivo",
|
||||
"s2631": "Olhando uma imagem",
|
||||
"s2632": "Imagem vista",
|
||||
"s2633": "Não foi possível ver a imagem",
|
||||
"s2634": "Procurando arquivos",
|
||||
"s2635": "Arquivos procurados",
|
||||
"s2636": "Não foi possível procurar arquivos",
|
||||
"s2637": "Procurando dentro dos arquivos",
|
||||
"s2638": "Busca dentro dos arquivos feita",
|
||||
"s2639": "Não foi possível buscar dentro dos arquivos",
|
||||
"s2640": "Executando um comando",
|
||||
"s2641": "Comando executado",
|
||||
"s2642": "O comando falhou",
|
||||
"s2643": "Abrindo uma página da web",
|
||||
"s2644": "Página da web aberta",
|
||||
"s2645": "Não foi possível abrir a página da web",
|
||||
"s2646": "Pesquisando na web",
|
||||
"s2647": "Pesquisa na web feita",
|
||||
"s2648": "Não foi possível pesquisar na web",
|
||||
"s2649": "Atualizando a lista de tarefas",
|
||||
"s2650": "Lista de tarefas atualizada",
|
||||
"s2651": "Não foi possível atualizar a lista de tarefas",
|
||||
"s2652": "Mostrando um resultado",
|
||||
"s2653": "Resultado mostrado",
|
||||
"s2654": "Não foi possível mostrar o resultado",
|
||||
"s2655": "Verificando a pasta",
|
||||
"s2656": "Pasta verificada",
|
||||
"s2657": "Não foi possível verificar a pasta",
|
||||
"s2658": "Verificando tarefas em segundo plano",
|
||||
"s2659": "Tarefas em segundo plano verificadas",
|
||||
"s2660": "Não foi possível verificar as tarefas em segundo plano",
|
||||
"s2661": "Lendo uma tarefa em segundo plano",
|
||||
"s2662": "Tarefa em segundo plano lida",
|
||||
"s2663": "Não foi possível ler a tarefa em segundo plano",
|
||||
"s2664": "Parando uma tarefa em segundo plano",
|
||||
"s2665": "Tarefa em segundo plano parada",
|
||||
"s2666": "Não foi possível parar a tarefa em segundo plano",
|
||||
"s2667": "Usando uma ferramenta",
|
||||
"s2668": "Ferramenta usada",
|
||||
"s2669": "A ferramenta falhou",
|
||||
"s2670": "Aplicar configurações e carregar",
|
||||
"s2671": "Aplicar e carregar",
|
||||
"s2672": "Os arquivos do modelo já estão neste computador. Nada será baixado; apenas as configurações escolhidas serão aplicadas.",
|
||||
"s2673": "Os modelos nativos atualmente carregados serão descarregados primeiro."
|
||||
}
|
||||
@@ -2619,5 +2619,57 @@
|
||||
"s2618": "Разрешено вносить изменения",
|
||||
"s2619": "Задача выполнена",
|
||||
"s2620": "Задаче нужно ваше согласие",
|
||||
"s2621": "Задача не удалась"
|
||||
"s2621": "Задача не удалась",
|
||||
"s2622": "Записываю файл",
|
||||
"s2623": "Файл записан",
|
||||
"s2624": "Не удалось записать файл",
|
||||
"s2625": "Изменяю файл",
|
||||
"s2626": "Файл изменён",
|
||||
"s2627": "Не удалось изменить файл",
|
||||
"s2628": "Читаю файл",
|
||||
"s2629": "Файл прочитан",
|
||||
"s2630": "Не удалось прочитать файл",
|
||||
"s2631": "Смотрю изображение",
|
||||
"s2632": "Изображение просмотрено",
|
||||
"s2633": "Не удалось посмотреть изображение",
|
||||
"s2634": "Ищу файлы",
|
||||
"s2635": "Файлы найдены",
|
||||
"s2636": "Не удалось найти файлы",
|
||||
"s2637": "Ищу внутри файлов",
|
||||
"s2638": "Поиск внутри файлов выполнен",
|
||||
"s2639": "Не удалось выполнить поиск внутри файлов",
|
||||
"s2640": "Выполняю команду",
|
||||
"s2641": "Команда выполнена",
|
||||
"s2642": "Команда не выполнилась",
|
||||
"s2643": "Открываю веб-страницу",
|
||||
"s2644": "Веб-страница открыта",
|
||||
"s2645": "Не удалось открыть веб-страницу",
|
||||
"s2646": "Ищу в интернете",
|
||||
"s2647": "Поиск в интернете выполнен",
|
||||
"s2648": "Не удалось выполнить поиск в интернете",
|
||||
"s2649": "Обновляю список дел",
|
||||
"s2650": "Список дел обновлён",
|
||||
"s2651": "Не удалось обновить список дел",
|
||||
"s2652": "Показываю результат",
|
||||
"s2653": "Результат показан",
|
||||
"s2654": "Не удалось показать результат",
|
||||
"s2655": "Проверяю папку",
|
||||
"s2656": "Папка проверена",
|
||||
"s2657": "Не удалось проверить папку",
|
||||
"s2658": "Проверяю фоновые задачи",
|
||||
"s2659": "Фоновые задачи проверены",
|
||||
"s2660": "Не удалось проверить фоновые задачи",
|
||||
"s2661": "Читаю фоновую задачу",
|
||||
"s2662": "Фоновая задача прочитана",
|
||||
"s2663": "Не удалось прочитать фоновую задачу",
|
||||
"s2664": "Останавливаю фоновую задачу",
|
||||
"s2665": "Фоновая задача остановлена",
|
||||
"s2666": "Не удалось остановить фоновую задачу",
|
||||
"s2667": "Использую инструмент",
|
||||
"s2668": "Инструмент использован",
|
||||
"s2669": "Инструмент не сработал",
|
||||
"s2670": "Применить настройки и загрузить",
|
||||
"s2671": "Применить и загрузить",
|
||||
"s2672": "Файлы модели уже на этом компьютере. Ничего не будет скачиваться, применяются только выбранные настройки.",
|
||||
"s2673": "Сначала будут выгружены все загруженные сейчас нативные модели."
|
||||
}
|
||||
@@ -2619,5 +2619,57 @@
|
||||
"s2618": "允许进行更改",
|
||||
"s2619": "任务已完成",
|
||||
"s2620": "任务需要你的确认",
|
||||
"s2621": "任务失败"
|
||||
"s2621": "任务失败",
|
||||
"s2622": "正在写入文件",
|
||||
"s2623": "已写入文件",
|
||||
"s2624": "无法写入文件",
|
||||
"s2625": "正在修改文件",
|
||||
"s2626": "已修改文件",
|
||||
"s2627": "无法修改文件",
|
||||
"s2628": "正在读取文件",
|
||||
"s2629": "已读取文件",
|
||||
"s2630": "无法读取文件",
|
||||
"s2631": "正在查看图片",
|
||||
"s2632": "已查看图片",
|
||||
"s2633": "无法查看图片",
|
||||
"s2634": "正在查找文件",
|
||||
"s2635": "已查找文件",
|
||||
"s2636": "无法查找文件",
|
||||
"s2637": "正在搜索文件内容",
|
||||
"s2638": "已搜索文件内容",
|
||||
"s2639": "无法搜索文件内容",
|
||||
"s2640": "正在运行命令",
|
||||
"s2641": "已运行命令",
|
||||
"s2642": "命令运行失败",
|
||||
"s2643": "正在打开网页",
|
||||
"s2644": "已打开网页",
|
||||
"s2645": "无法打开网页",
|
||||
"s2646": "正在搜索网络",
|
||||
"s2647": "已搜索网络",
|
||||
"s2648": "无法搜索网络",
|
||||
"s2649": "正在更新待办清单",
|
||||
"s2650": "已更新待办清单",
|
||||
"s2651": "无法更新待办清单",
|
||||
"s2652": "正在向你展示结果",
|
||||
"s2653": "已向你展示结果",
|
||||
"s2654": "无法展示结果",
|
||||
"s2655": "正在查看文件夹",
|
||||
"s2656": "已查看文件夹",
|
||||
"s2657": "无法查看文件夹",
|
||||
"s2658": "正在查看后台任务",
|
||||
"s2659": "已查看后台任务",
|
||||
"s2660": "无法查看后台任务",
|
||||
"s2661": "正在读取后台任务",
|
||||
"s2662": "已读取后台任务",
|
||||
"s2663": "无法读取后台任务",
|
||||
"s2664": "正在停止后台任务",
|
||||
"s2665": "已停止后台任务",
|
||||
"s2666": "无法停止后台任务",
|
||||
"s2667": "正在使用工具",
|
||||
"s2668": "已使用工具",
|
||||
"s2669": "工具运行失败",
|
||||
"s2670": "应用设置并加载",
|
||||
"s2671": "应用并加载",
|
||||
"s2672": "模型文件已在这台电脑上,不会下载任何内容,只会应用你选择的设置。",
|
||||
"s2673": "当前已加载的本地模型会先被卸载。"
|
||||
}
|
||||
@@ -622,12 +622,13 @@ function ToolCard({ messageId, tool: t }: {
|
||||
const scope = SCOPE[t.scope];
|
||||
const pending = t.state === 'proposed';
|
||||
if (!pending && t.state !== 'running')
|
||||
return <details className="disclosure tool-summary"><summary><Icon name="chevron" size={14}/>{t.name}<span>·</span><Icon name={t.state === 'denied' ? 'x' : 'check'} size={14}/>{tr(t.state === 'done' && t.scope !== 'read' ? (t.live && !t.approved ? 'Ran' : 'Approved') : t.state === 'denied' ? 'Denied' : t.state === 'done' ? 'Done' : t.state)}</summary><div><code>{t.preview}</code><p className="small muted">{tr(t.result ?? t.effect)}</p></div></details>;
|
||||
return <details className="disclosure tool-summary"><summary><Icon name="chevron" size={14}/>{tr(t.verb ?? t.name)}{t.target ? <><span>·</span><span className="tool-summary__target" title={t.target}>{t.target}</span></> : null}<span>·</span><Icon name={t.state === 'denied' || t.state === 'error' ? 'x' : 'check'} size={14}/>{tr(t.state === 'denied' ? 'Denied' : t.state === 'error' ? 'Failed' : t.live && t.state === 'done' && t.approved ? 'Approved' : 'Done')}</summary><div><code>{t.preview}</code><p className="small muted">{tr(t.result ?? t.effect)}</p></div></details>;
|
||||
return <section className="toolcard toolcard--compact" data-state={t.state} aria-label={t.name}>
|
||||
<details className="toolcard__disclosure">
|
||||
<summary><Icon name={t.scope === 'shell' ? 'bolt' : 'tool'} size={17}/><span className="toolcard__title">{t.name}</span><span className="toolcard__scope xs muted">{tr(scope.label)}</span><Icon name="chevron" className="toolcard__chevron" size={15}/></summary>
|
||||
<summary><Icon name={t.scope === 'shell' ? 'bolt' : 'tool'} size={17}/><span className="toolcard__title">{tr(t.verb ?? t.name)}</span><span className="toolcard__scope xs muted">{tr(scope.label)}</span><Icon name="chevron" className="toolcard__chevron" size={15}/></summary>
|
||||
<div className="toolcard__body"><code className="toolcard__preview">{t.preview}</code><p className="small muted">{tr(t.effect)}</p>{tr(t.result ? <p className="small">{t.result}</p> : null)}</div>
|
||||
</details>
|
||||
{tr(pending && t.target ? <code className="toolcard__what" title={t.target}>{t.target}</code> : null)}
|
||||
{tr(pending ? <div className="toolcard__quick-actions"><span className="xs muted">{tr("Needs your approval")}</span><Btn size="sm" onClick={() => resolveTool(messageId, t.id, false)}>{tr("Deny")}</Btn><Btn variant="primary" size="sm" icon="check" onClick={() => resolveTool(messageId, t.id, true)}>{tr("Approve")}</Btn></div> : <span className="xs muted toolcard__running" role="status">{tr("Running\u2026")}</span>)}
|
||||
</section>;
|
||||
}
|
||||
|
||||
@@ -7009,3 +7009,7 @@ textarea {
|
||||
/* What you type and the small print under each message follow the reading size too, so the whole conversation changes together. */
|
||||
.st-composer textarea { font-size: var(--st-content-fs, 14px); }
|
||||
.st-msg__meta, .st-msg__meta time { font-size: calc(var(--st-content-fs, 14px) * .8125); }
|
||||
|
||||
/* What an approval is about, shown without opening the card: the file or the command. */
|
||||
.toolcard__what { display: block; margin: 4px 0 2px; padding: 6px 10px; background: var(--st-surface-2); color: var(--st-ink); border: 1px solid var(--st-line); border-radius: var(--st-r-sm); white-space: pre-wrap; overflow-wrap: anywhere; max-height: 6.5em; overflow: auto; font-size: 12px; }
|
||||
.tool-summary__target { min-width: 0; max-width: 28em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--st-mono, monospace); font-size: 12px; }
|
||||
@@ -238,3 +238,16 @@ test('Mode over HTTP: the chat body carries the Mode menu choice; Plan refuses a
|
||||
assert.equal(lines(join(s.ws, 'manual-http.txt')), 1);
|
||||
assert.equal(manual.filter(f => f.type === 'agent-tool').at(-1).tool.approved, true);
|
||||
});
|
||||
|
||||
test('item 9: a chat that asks for no tools never runs one (agent:false and tools:false answer directly)', { skip }, async t => {
|
||||
const s = await setup(t);
|
||||
for (const [chatId, flag] of [['chat-plain-a', { agent: false }], ['chat-plain-t', { tools: false }]]) {
|
||||
s.worker.requests.length = 0;
|
||||
const frames = await s.host.chat({ sessionId: chatId, message: 'SCRIPT:tool plain-never.txt x', remoteModel: s.remoteModel, workspaceId: null, history: [], ...flag });
|
||||
assert.equal(frames.filter(f => f.type === 'agent-tool').length, 0, `${chatId}: no tool card`);
|
||||
assert.ok(!frames.some(f => f.type === 'start' && f.agent === true), `${chatId}: not an agent turn`);
|
||||
assert.ok(!frames.some(f => f.type === 'notice' && f.code === 'agent-skipped'), `${chatId}: asking for plain chat is not a surprise to explain`);
|
||||
assert.equal(existsSync(join(s.ws, 'plain-never.txt')), false);
|
||||
assert.ok(s.worker.requests.length >= 1 && s.worker.requests.every(r => r.toolNames.length === 0), `${chatId}: the model was offered no tool`);
|
||||
}
|
||||
});
|
||||
@@ -105,7 +105,7 @@ test('the agent is offered exactly these tools: everything useful, no scheduler,
|
||||
const r = await rig(t);
|
||||
await r.run('inv', 'SCRIPT:echo hi');
|
||||
const offered = [...r.worker.requests].reverse().find(q => q.toolNames.length)?.toolNames.sort();
|
||||
assert.deepEqual(offered, ['create_goal', 'edit', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'present', 'pwsh', 'read', 'read_image', 'send_message', 'skill', 'subagent', 'subagent_fork', 'todo_write', 'update_goal', 'web_fetch', 'workflow', 'working_directory', 'write'].sort());
|
||||
assert.deepEqual(offered, ['edit', 'glob', 'grep', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'present', 'pwsh', 'read', 'read_image', 'send_message', 'subagent', 'todo_write', 'web_fetch', 'working_directory', 'write'].sort());
|
||||
assert.ok(!offered.some(n => n.startsWith('schedule_')) && !offered.includes('web_search') && !offered.includes('ask_user_question') && !offered.includes('exit_plan_mode'));
|
||||
});
|
||||
|
||||
@@ -120,10 +120,9 @@ test('a tool from each family runs and shows a card the page can render', { skip
|
||||
assert.match(last(await r.call('fam-s', 'grep', { pattern: 'needle' })).result, /Line 2/);
|
||||
assert.equal(last(await r.call('fam-w', 'working_directory', {})).state, 'done');
|
||||
assert.equal(last(await r.call('fam-j', 'job_list', {})).state, 'done');
|
||||
// plan / goal / list family
|
||||
// plan / list family
|
||||
assert.equal(last(await r.call('fam-t', 'todo_write', { todos: [{ content: 'check the build', status: 'pending' }] })).state, 'done');
|
||||
const goal = last(await r.call('fam-goal', 'create_goal', { objective: 'finish the notes' }));
|
||||
assert.equal(goal.state, 'done'); assert.equal(goal.effect, 'Sets a goal to work towards.');
|
||||
// (the goal tools are not offered since the prompt trim: tests/functional-fixes.test.mjs pins that)
|
||||
// write family (Bypass: no card asks)
|
||||
const wrote = await r.call('fam-fs', 'write', { file_path: fwd(join(r.ws, 'new.txt')), content: 'made\n' });
|
||||
assert.equal(last(wrote).state, 'done'); assert.equal(readFileSync(join(r.ws, 'new.txt'), 'utf8').trim(), 'made');
|
||||
|
||||
@@ -28,7 +28,7 @@ test('the approval and result texts of a chat card exist in every language, tran
|
||||
test('the card shows "Ran" unless the person approved, and never "Demo approval"', () => {
|
||||
const chat = readFileSync(resolve(root, 'src/screens/Chat.tsx'), 'utf8');
|
||||
assert.ok(!/Demo approval/i.test(chat));
|
||||
assert.match(chat, /t\.live && !t\.approved \? 'Ran' : 'Approved'/);
|
||||
assert.match(chat, /t\.live && t\.state === 'done' && t\.approved \? 'Approved' : 'Done'/);
|
||||
const store = readFileSync(resolve(root, 'src/lib/store.ts'), 'utf8');
|
||||
assert.ok(!/Demo approval/i.test(store));
|
||||
assert.match(store, /mode:state\.sessions\.find\(s=>s\.id===sessionId\)\?\.preferences\?\.mode\?\?'manual'/, 'the Mode menu choice travels with the turn');
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Fixes from the real-engine functional test of the integrated build (notes-split-uitest.md, "Functional (real engine)"):
|
||||
* tok/s and context in the agent path, plain-language tool rows, a launch that reports success, and the size and wording of
|
||||
* what the agent sends to the model. The model is scripted; the Harness, the bridge and the projector are real.
|
||||
*/
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { TurnProjector, outcomeText, toolVerb } from '../server/agent/events.mjs';
|
||||
import { AGENT_PROMPT } from '../server/agent/preset.mjs';
|
||||
import { startScriptedWorker } from './helpers/scripted-worker.mjs';
|
||||
import { createAgentBridge } from '../server/agent/agent-bridge.mjs';
|
||||
import { resolveHarness } from '../server/agent/harness-runtime.mjs';
|
||||
|
||||
const HARNESS = process.env.STRATA_HARNESS_DIR;
|
||||
const skip = !HARNESS || !resolveHarness({ env: process.env }).ok ? 'STRATA_HARNESS_DIR is not set to a built Harness' : false;
|
||||
const cleanup = dir => { try { rmSync(dir, { recursive: true, force: true, maxRetries: 3 }); } catch { /* best effort */ } };
|
||||
const ev = (type, data, seq = 1, time = 1000) => ({ type: 'event', event: { type, seq, time, data } });
|
||||
const chunk = (c, attemptId = 'a:1') => ({ type: 'assistant-stream', frame: { type: 'chunk', attemptId, chunk: c } });
|
||||
|
||||
/** Run `fn` with a clock the test moves by hand. */
|
||||
function withClock(fn) {
|
||||
const real = Date.now; let now = 1_000_000;
|
||||
Date.now = () => now;
|
||||
try { return fn(ms => { now += ms; }); } finally { Date.now = real; }
|
||||
}
|
||||
|
||||
test('item 1: the agent path reports live tok/s while it streams and a final tok/s, not null', () => withClock(tick => {
|
||||
const p = new TurnProjector({ startedAt: Date.now() });
|
||||
const frames = [];
|
||||
// 60 chunks of one token each, 25 ms apart: 40 tokens per second.
|
||||
for (let i = 0; i < 60; i++) { tick(25); frames.push(...p.consume(chunk({ type: 'text-delta', index: 0, text: 'ab' }))); }
|
||||
const stats = frames.filter(f => f.type === 'stats');
|
||||
assert.ok(stats.length >= 2, 'live speed frames while the answer streams');
|
||||
const last = stats.at(-1);
|
||||
assert.equal(last.source, 'stream');
|
||||
assert.ok(last.tps > 30 && last.tps < 50, `live tps ${last.tps}`);
|
||||
p.consume(chunk({ type: 'usage', usage: { inputTokens: 900, outputTokens: 60 } }));
|
||||
p.consume(ev('turn/end', { turn: 1, reason: { kind: 'completed' } }));
|
||||
const done = p.finish({ model: 'M', contextMax: 65536 }).find(f => f.type === 'done');
|
||||
assert.ok(done.tps > 30 && done.tps < 50, `final tps ${done.tps}`);
|
||||
assert.equal(done.tokensOut, 60);
|
||||
}));
|
||||
|
||||
test('item 1: reasoning tokens count towards the speed too (a thinking model streams reasoning first)', () => withClock(tick => {
|
||||
const p = new TurnProjector({ startedAt: Date.now() });
|
||||
for (let i = 0; i < 40; i++) { tick(20); p.consume(chunk({ type: 'reasoning-delta', index: 0, text: 'x' })); }
|
||||
p.consume(ev('turn/end', { turn: 1, reason: { kind: 'completed' } }));
|
||||
const done = p.finish({}).find(f => f.type === 'done');
|
||||
assert.ok(done.tps > 40 && done.tps < 60, `tps ${done.tps}`);
|
||||
}));
|
||||
|
||||
test('item 2: the context is the LAST request (prompt + reply), not the sum of every step', () => {
|
||||
const p = new TurnProjector({ startedAt: 0 });
|
||||
p.consume(chunk({ type: 'usage', usage: { inputTokens: 5000, outputTokens: 100 } }, 'a:1'));
|
||||
p.consume(chunk({ type: 'usage', usage: { inputTokens: 5200, outputTokens: 300 } }, 'a:2'));
|
||||
p.consume(chunk({ type: 'usage', usage: { inputTokens: 5600, outputTokens: 50 } }, 'a:3'));
|
||||
p.consume(ev('turn/end', { turn: 1, reason: { kind: 'completed' } }));
|
||||
const done = p.finish({ model: 'M', contextMax: 65536 }).find(f => f.type === 'done');
|
||||
assert.equal(done.contextTokens, 5650, 'what the context holds after the turn');
|
||||
assert.equal(done.tokensIn, 15800, 'the billed sum is still reported separately');
|
||||
});
|
||||
|
||||
test('item 7: tool rows say what happened in plain words, with what it was about', () => {
|
||||
const p = new TurnProjector({ startedAt: 0 });
|
||||
const call = p.consume(ev('tool/call', { callId: 'w1', name: 'write', arguments: JSON.stringify({ file_path: 'C:/work/notes.md', content: 'x' }) }))[0].tool;
|
||||
assert.equal(call.verb, 'Writing a file'); assert.equal(call.target, 'C:/work/notes.md');
|
||||
const asked = p.consume(ev('approval/asked', { id: 'a1', callId: 'w1', reason: 'Create or replace the file C:/work/notes.md' }))[0].tool;
|
||||
assert.equal(asked.state, 'proposed'); assert.equal(asked.target, 'C:/work/notes.md', 'the approval card shows the file');
|
||||
const ok = p.consume(ev('tool/result', { message: { toolCallId: 'w1', content: [{ type: 'text', text: 'written' }] } }))[0].tool;
|
||||
assert.equal(ok.verb, 'Wrote a file');
|
||||
p.consume(ev('tool/call', { callId: 'w2', name: 'write', arguments: JSON.stringify({ file_path: 'D:/x.txt' }) }));
|
||||
const bad = p.consume(ev('tool/result', { message: { toolCallId: 'w2', isError: true, content: [{ type: 'text', text: 'denied' }] } }))[0].tool;
|
||||
assert.equal(bad.state, 'error'); assert.equal(bad.verb, "Couldn't write the file");
|
||||
const sh = p.consume(ev('tool/call', { callId: 's1', name: 'pwsh', arguments: JSON.stringify({ command: 'Get-ChildItem C:/work' }) }))[0].tool;
|
||||
assert.equal(sh.verb, 'Running a command'); assert.equal(sh.target, 'Get-ChildItem C:/work');
|
||||
const g = p.consume(ev('tool/call', { callId: 'g1', name: 'glob', arguments: JSON.stringify({ pattern: '**/*.md' }) }))[0].tool;
|
||||
assert.equal(g.verb, 'Searching for files'); assert.equal(g.target, '**/*.md');
|
||||
assert.equal(toolVerb('glob', 'done'), 'Searched for files');
|
||||
assert.equal(toolVerb('some_connected_tool', 'running'), 'Using a tool');
|
||||
for (const name of ['write', 'edit', 'read', 'glob', 'grep', 'pwsh', 'bash', 'web_fetch', 'todo_write']) {
|
||||
for (const state of ['running', 'done', 'error']) assert.ok(!/^(write|edit|read|glob|grep|pwsh|bash|web_fetch|todo_write)\b/.test(toolVerb(name, state)), `${name} ${state}: no raw tool name`);
|
||||
}
|
||||
});
|
||||
|
||||
test('item 8: starting a program that prints nothing is reported as started, plainly', () => {
|
||||
for (const command of ['Start-Process explorer.exe C:\\work', 'explorer.exe C:\\work', 'start notepad', 'Invoke-Item C:\\work\\a.pdf', 'ii .', 'xdg-open .', 'open .']) {
|
||||
assert.match(outcomeText('pwsh', '(no output)', { command }), /^Started\./, command);
|
||||
}
|
||||
assert.match(outcomeText('pwsh', '(no output)', { command: 'New-Item x.txt | Out-Null' }), /finished without any error/, 'other silent commands keep their wording');
|
||||
assert.equal(outcomeText('pwsh', 'hello', { command: 'echo hello' }), 'hello', 'a command that printed something shows it');
|
||||
const p = new TurnProjector({ startedAt: 0 });
|
||||
p.consume(ev('tool/call', { callId: 'e1', name: 'pwsh', arguments: JSON.stringify({ command: 'Start-Process explorer.exe C:\\work' }) }));
|
||||
const res = p.consume(ev('tool/result', { message: { toolCallId: 'e1', content: [{ type: 'text', text: '(no output)' }] } }))[0].tool;
|
||||
assert.match(res.result, /^Started\./); assert.equal(res.verb, 'Ran a command');
|
||||
});
|
||||
|
||||
test('item 7/10: our prompt has no internal terms and is short', () => {
|
||||
assert.ok(AGENT_PROMPT.length < 1600, `${AGENT_PROMPT.length} chars`);
|
||||
assert.doesNotMatch(AGENT_PROMPT, /DSH|DeepSeek|sandbox|isolated session|file policy|Harness/i);
|
||||
assert.match(AGENT_PROMPT, /do not say you could not confirm/i);
|
||||
});
|
||||
|
||||
test('items 3, 7, 10: what the agent sends the model is small, plain, and carries Thinking Off', { skip }, async t => {
|
||||
const worker = await startScriptedWorker({});
|
||||
const dir = mkdtempSync(join(tmpdir(), 'strata-fx-'));
|
||||
const ws = join(dir, 'ws'); mkdirSync(ws, { recursive: true });
|
||||
const local = [{ id: 'i', baseUrl: worker.url, modelId: 'scripted-model', context: 65536 }];
|
||||
const bridge = createAgentBridge({ dataDir: dir, localInstances: () => local, connections: () => [], env: process.env, startTimeoutMs: 90_000 });
|
||||
t.after(async () => { await bridge.close(); await worker.close(); cleanup(dir); });
|
||||
worker.requests.length = 0;
|
||||
await bridge.runTurn({ chatId: 'fx-1', message: 'SCRIPT:echo hi', workspacePath: ws, mode: 'plan', thinking: 'Off', onFrame: () => {} });
|
||||
const req = worker.requests.find(r => r.toolNames.length);
|
||||
assert.ok(req, 'a request with tools');
|
||||
assert.equal(req.params.chat_template_kwargs.enable_thinking, false, 'Thinking Off reaches the model request');
|
||||
for (const gone of ['workflow', 'create_goal', 'update_goal', 'get_goal', 'skill', 'subagent_fork']) assert.ok(!req.toolNames.includes(gone), `${gone} is not sent`);
|
||||
for (const kept of ['pwsh', 'read', 'write', 'edit', 'glob', 'grep']) assert.ok(req.toolNames.includes(kept), `${kept} is still there`);
|
||||
// 29,700 characters before the trim (about 7,500 tokens); the request now stays below 20,000 including the user's own message.
|
||||
assert.ok(req.promptChars < 20_000, `first request is ${req.promptChars} characters`);
|
||||
});
|
||||
@@ -169,5 +169,26 @@ class EffortPaths(unittest.TestCase):
|
||||
self.assertEqual(self.level("/v1/chat/completions", {"messages": history, "reasoning_effort": given}), want)
|
||||
|
||||
|
||||
def test_the_agents_request_shape_with_thinking_off(self):
|
||||
"""The app's agent (Harness) sends no reasoning_effort when Thinking is Off, only chat_template_kwargs {enable_thinking: false}, with tools, a
|
||||
system message, earlier reasoning and a token cap. That one flag must reach the template as the empty think block, whatever else is in the
|
||||
request or configured (a thinking model that went on reasoning with the menu on Off was this request form). The same body with
|
||||
enable_thinking true / an effort keeps thinking."""
|
||||
tools = [{"type": "function", "function": {"name": "pwsh", "description": "d", "parameters": {"type": "object", "properties": {}}}}]
|
||||
history = [{"role": "developer", "content": "You are a helpful assistant."}, {"role": "user", "content": "first"},
|
||||
{"role": "assistant", "content": "ok", "reasoning_content": "earlier thoughts"}, {"role": "user", "content": "now?"}]
|
||||
agent = {"model": "m", "messages": history, "tools": tools, "stream": True, "stream_options": {"include_usage": True},
|
||||
"max_completion_tokens": 4096}
|
||||
for config in (None, "high", "low"):
|
||||
self.svc.config_effort = config
|
||||
with self.subTest(config=config):
|
||||
self.assertEqual(self.level("/v1/chat/completions", {**agent, "chat_template_kwargs": {"enable_thinking": False}}), "none")
|
||||
self.svc.config_effort = None
|
||||
self.assertEqual(self.level("/v1/chat/completions", {**agent, "chat_template_kwargs": {"enable_thinking": True, "reasoning_effort": "low"}}), "low")
|
||||
self.assertEqual(self.level("/v1/chat/completions", {**agent, "chat_template_kwargs": {"enable_thinking": True, "reasoning_effort": "high"}}), "xhigh")
|
||||
# the same flag in the plain (no tools) body the direct chat sends alongside a budget
|
||||
self.assertEqual(self.level("/v1/chat/completions", {"messages": MSG, "chat_template_kwargs": {"enable_thinking": False}, "reasoning_effort": "none"}), "none")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user