mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
fix(studio): the structural gates are held to the design as it is: SegChoice and PillButton name their action through a certified prop, the shared Menu row's callback is followed into its parent, the slider, strip scroll, dialogs and plan picker carry action names, keyframes, radii, orphan components, translations and settings rows are brought back in line, and the phone layout no longer hides the branch chip from the hover guard
This commit is contained in:
1 parent
0fac934b3f
commit
e386c45525
27 files changed
+90
-231
No files matched your search
@@ -38,6 +38,8 @@ try {
|
||||
check(`${context}: one hover ground across the row`, grounds.size === 1, [...grounds].join(" / "));
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto(url.href, { waitUntil: "networkidle" });
|
||||
const more = page.locator(".compose .turn-more");
|
||||
if (await more.isVisible()) await more.click();
|
||||
const chip = page.locator(turns ? ".compose .studio-branch" : ".compose .worktree-trigger").first();
|
||||
await chip.click();
|
||||
check(`${context}: a narrow-pane pointer click opens the workspace menu`, await chip.getAttribute("aria-expanded") === "true");
|
||||
|
||||
@@ -56,7 +56,7 @@ const undeclared = census.roots.filter((r) => !r.actions.length);
|
||||
// denominator that grows because the instrument was corrected is not the same
|
||||
// event as one that grows because debt was added, and numbers either side of
|
||||
// that change do not compare.
|
||||
const UNDECLARED_CEILING = 217;
|
||||
const UNDECLARED_CEILING = 210;
|
||||
|
||||
/** One file, for holding the discovery rules themselves. */
|
||||
const only = (src: string) => certifyRoots(new Map([["src/ui/X.tsx", src]]), { resolve });
|
||||
@@ -215,7 +215,7 @@ describe("what a certified primitive's call site counts as", () => {
|
||||
it("rests on that declaration: production loses those roots without it", () => {
|
||||
const blind = certifyRoots(sources, {
|
||||
resolve,
|
||||
primitives: PRIMITIVES.map((p) => (p.file === "ui/Menu.tsx" ? { ...p, callbacks: {} } : p)),
|
||||
primitives: PRIMITIVES.map((p) => ({ ...p, callbacks: Object.fromEntries(Object.entries(p.callbacks).filter(([prop]) => prop !== "onPick")) })),
|
||||
});
|
||||
const picks = (c: { roots: { prop?: string; actions: string[] }[] }) => c.roots.filter((r) => r.prop === "onPick");
|
||||
expect(picks(census).length).toBeGreaterThan(0);
|
||||
|
||||
@@ -127,7 +127,6 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "workspace.source-open", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "workspace.worktree-toggle", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "workspace.search", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "session.view", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "session.filter", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "session.recent", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "session.recent-environment", kind: "view", target: "none", proof: "interaction" },
|
||||
@@ -166,7 +165,6 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "metrics.hide-amounts", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "session.menu", kind: "view", target: "entity", proof: "interaction" },
|
||||
{ id: "workspace.menu", kind: "view", target: "entity", proof: "interaction" },
|
||||
{ id: "machine.menu", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "session.pin", kind: "view", target: "entity", proof: "interaction" },
|
||||
{ id: "session.mark-read", kind: "kernel-mutation", target: "entity", proof: "interaction" },
|
||||
{ id: "session.pause", kind: "kernel-mutation", target: "entity", proof: "interaction" },
|
||||
@@ -244,7 +242,6 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "git.branch", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
|
||||
// ── What is waiting to be sent ───────────────────────────────────────────
|
||||
{ id: "queue.edit", kind: "kernel-mutation", target: "entity", proof: "interaction" },
|
||||
// Giving a session a name of your own. The sidebar's tree and the pane tabs
|
||||
// rename the same session through the same host call, so it is one id.
|
||||
{ id: "session.auto-name", kind: "kernel-mutation", target: "entity", proof: "authority-effect" },
|
||||
@@ -254,8 +251,6 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "queue.cancel", kind: "kernel-mutation", target: "entity", proof: "interaction" },
|
||||
{ id: "queue.send-now", kind: "kernel-mutation", target: "entity", proof: "interaction" },
|
||||
{ id: "queue.retry", kind: "repeatable", target: "entity", proof: "interaction" },
|
||||
{ id: "queue.refresh", kind: "repeatable", target: "entity", proof: "interaction" },
|
||||
{ id: "queue.move", kind: "kernel-mutation", target: "entity", proof: "interaction" },
|
||||
{ id: "queue.pause", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
|
||||
// ── Answers the host is blocked on. One id per port call, with the verdict
|
||||
@@ -264,6 +259,14 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "decision.tool", kind: "interaction", target: "entity", proof: "authority-effect" },
|
||||
{ id: "decision.full-access", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "decision.plan", kind: "interaction", target: "entity", proof: "authority-effect" },
|
||||
{ id: "decision.more", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "composer.more-tools", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "ext.scope", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "ext.scope-back", kind: "navigation", target: "none", proof: "interaction" },
|
||||
{ id: "session.import-native-choose", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "session.filter-clear", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "queue.to-steer", kind: "kernel-mutation", target: "entity", proof: "interaction" },
|
||||
{ id: "steer.withdraw", kind: "kernel-mutation", target: "entity", proof: "interaction" },
|
||||
|
||||
// ── Capabilities ─────────────────────────────────────────────────────────
|
||||
{ id: "skill.enabled", kind: "kernel-mutation", target: "entity", proof: "authority-effect" },
|
||||
@@ -657,6 +660,8 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "settings.search", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "workspace.isolate", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "reasoning.effort", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "reasoning.effort-preview", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "settings.sections-scroll", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "files.open", kind: "navigation", target: "none", proof: "interaction" },
|
||||
{ id: "palette.pick", kind: "view", target: "entity", proof: "interaction" },
|
||||
{ id: "palette.scope", kind: "view", target: "none", proof: "interaction" },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[data-fold~="phone"] .composer-menu.studio-model-menu.menu-sheet {
|
||||
position: fixed; inset: auto 0 0 0; width: auto; max-width: none; max-height: min(82vh, 640px); box-sizing: border-box; margin: 0;
|
||||
display: flex; flex-direction: column; overflow: hidden; padding: 8px 10px 18px; border: 0; border-radius: 15px 15px 0 0;
|
||||
display: flex; flex-direction: column; overflow: hidden; padding: 8px 10px 18px; border: 0; border-radius: var(--r-lg) var(--r-lg) 0 0;
|
||||
background: var(--surface); box-shadow: var(--shadow-float), 0 0 0 100vmax rgb(0 0 0 / .3);
|
||||
transform: none; transition: none; animation: sheet-up var(--m-panel) both;
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
[data-fold~="phone"] .composer-menu.studio-model-menu.menu-sheet .studio-picker-title small { display: none; }
|
||||
[data-fold~="phone"] .composer-menu.studio-model-menu.menu-sheet .mchips { display: none; }
|
||||
[data-fold~="phone"] .composer-menu.studio-model-menu.menu-sheet :is(.mfind, input[type="search"]) { box-sizing: border-box; width: calc(100% - 8px); margin: 0 4px 8px; }
|
||||
[data-fold~="phone"] .composer-menu.studio-model-menu.menu-sheet button.mi { box-sizing: border-box; min-height: 36px; gap: 8px; padding: 0 10px; border-radius: 8px; }
|
||||
[data-fold~="phone"] .composer-menu.studio-model-menu.menu-sheet button.mi { box-sizing: border-box; min-height: 36px; gap: 8px; padding: 0 10px; border-radius: var(--r-sm); }
|
||||
[data-fold~="phone"] .composer-menu.studio-model-menu.menu-sheet button.mi .lb { font: var(--w-med) var(--fs-ui)/1.3 var(--mono); }
|
||||
[data-fold~="phone"] .composer-menu.studio-model-menu.menu-sheet button.mi .ds { color: var(--muted); font: var(--w-reg) var(--fs-meta)/1.3 var(--ui); }
|
||||
[data-fold~="phone"] .composer-menu.studio-model-menu.menu-sheet .menu-rows .mi.head { min-height: 26px; padding: 4px 10px; }
|
||||
@@ -28,7 +28,7 @@
|
||||
[data-fold~="phone"] .composer-menu.studio-model-menu.menu-sheet .menu-aside > .mi.head:last-child { display: none; }
|
||||
|
||||
@property --p { syntax: "<number>"; inherits: true; initial-value: 0; }
|
||||
.effort-slider { display: grid; gap: 12px; padding: 14px 18px 12px; overflow: hidden; border-radius: 14px; background: var(--overlay); }
|
||||
.effort-slider { display: grid; gap: 12px; padding: 14px 18px 12px; overflow: hidden; border-radius: var(--r-lg); background: var(--overlay); }
|
||||
.effort-slider .es-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
||||
.effort-slider .es-name { color: var(--text); font: var(--w-semi) var(--fs-lead)/1.3 var(--ui); }
|
||||
.effort-slider .es-model { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font: var(--w-reg) var(--fs-meta)/1.3 var(--mono); }
|
||||
@@ -40,7 +40,7 @@
|
||||
.effort-slider .es-tick[data-on] { border-color: var(--brand); background: var(--brand); }
|
||||
.effort-slider .es-thumb { position: absolute; z-index: 2; top: 0; left: calc(var(--p) * (100% - 28px)); box-sizing: border-box; display: grid; place-items: center; width: 28px; height: 28px; border: 2px solid var(--brand); border-radius: 50%; background: var(--raised); box-shadow: 0 1px 3px rgb(0 0 0 / .25); pointer-events: none; }
|
||||
.effort-slider .es-r { width: 14px; height: 14px; fill: none; stroke: var(--brand); stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.effort-slider input[type="range"] { -webkit-appearance: none; appearance: none; position: relative; z-index: 1; box-sizing: border-box; display: block; width: 100%; height: 28px; margin: 0; padding: 0; border: 0; border-radius: 14px; cursor: pointer; background: transparent; touch-action: none; }
|
||||
.effort-slider input[type="range"] { -webkit-appearance: none; appearance: none; position: relative; z-index: 1; box-sizing: border-box; display: block; width: 100%; height: 28px; margin: 0; padding: 0; border: 0; border-radius: var(--r-pill); cursor: pointer; background: transparent; touch-action: none; }
|
||||
.effort-slider input[type="range"]::-webkit-slider-runnable-track { height: 28px; background: transparent; }
|
||||
.effort-slider input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 28px; height: 28px; border: 0; border-radius: 50%; background: transparent; opacity: 0; }
|
||||
.effort-slider input[type="range"]::-moz-range-thumb { width: 28px; height: 28px; border: 0; border-radius: 50%; background: transparent; opacity: 0; }
|
||||
|
||||
@@ -19,7 +19,7 @@ describe("the model menu as a sheet on a phone", () => {
|
||||
const sheet = rule(S);
|
||||
expect(sheet).toMatch(/position:\s*fixed/);
|
||||
expect(sheet).toMatch(/inset:\s*auto 0 0 0/);
|
||||
expect(sheet).toMatch(/border-radius:\s*15px 15px 0 0/);
|
||||
expect(sheet).toMatch(/border-radius:\s*var\(--r-lg\) var\(--r-lg\) 0 0/);
|
||||
expect(sheet).toMatch(/padding:\s*8px 10px 18px/);
|
||||
expect(sheet).toMatch(/background:\s*var\(--surface\)/);
|
||||
expect(sheet).toMatch(/overflow:\s*hidden/);
|
||||
@@ -57,7 +57,7 @@ describe("the model menu as a sheet on a phone", () => {
|
||||
const row = rule(S + " button.mi");
|
||||
expect(row).toMatch(/min-height:\s*36px/);
|
||||
expect(row).toMatch(/padding:\s*0 10px/);
|
||||
expect(row).toMatch(/border-radius:\s*8px/);
|
||||
expect(row).toMatch(/border-radius:\s*var\(--r-sm\)/);
|
||||
expect(rule(S + " button.mi .lb")).toMatch(/font:\s*var\(--w-med\) var\(--fs-ui\)\/1\.3 var\(--mono\)/);
|
||||
expect(rule(S + " :is(.vendor-mark, .mi-mono)")).toMatch(/width:\s*16px/);
|
||||
expect(rule(S + " .menu-rows .mi.head")).toMatch(/min-height:\s*26px/);
|
||||
@@ -83,7 +83,7 @@ describe("the reasoning depth line", () => {
|
||||
|
||||
it("is a card whose header sets the level on the left and the model on the right", () => {
|
||||
expect(track("\n.effort-slider")).toMatch(/background:\s*var\(--overlay\)/);
|
||||
expect(track("\n.effort-slider")).toMatch(/border-radius:\s*14px/);
|
||||
expect(track("\n.effort-slider")).toMatch(/border-radius:\s*var\(--r-lg\)/);
|
||||
const head = track(".effort-slider .es-head");
|
||||
expect(head).toMatch(/display:\s*flex/);
|
||||
expect(head).toMatch(/justify-content:\s*space-between/);
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
.apv-more { display: none; }
|
||||
[data-fold~="phone"] .flow .call[data-approval] .apv-ft { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 10px 12px; }
|
||||
[data-fold~="phone"] .flow .call[data-approval] .apv-ft .btn { box-sizing: border-box; width: 100%; justify-self: stretch; height: 44px; min-height: 44px; display: grid; place-items: center; padding: 0 10px; border-radius: 10px; font: var(--w-med) var(--fs-lead)/1 var(--ui); }
|
||||
[data-fold~="phone"] .flow .call[data-approval] .apv-ft .btn { box-sizing: border-box; width: 100%; justify-self: stretch; height: 44px; min-height: 44px; display: grid; place-items: center; padding: 0 10px; border-radius: var(--r-md); font: var(--w-med) var(--fs-lead)/1 var(--ui); }
|
||||
[data-fold~="phone"] .flow .call[data-approval] .apv-ft .btn[data-deny] { order: 0; border: 1px solid var(--border); background: transparent; color: var(--err-ink); }
|
||||
[data-fold~="phone"] .flow .call[data-approval] .apv-ft .btn[data-primary] { order: 1; border: 0; background: var(--accent); color: var(--accent-fg); }
|
||||
[data-fold~="phone"] .flow .call[data-approval] .apv-ft .btn:not([data-primary], [data-deny]) { display: none; order: 2; grid-column: 1 / -1; }
|
||||
|
||||
@@ -73,7 +73,7 @@ describe("the approval card on a phone", () => {
|
||||
expect(rule(footer)).toMatch(/gap:\s*8px/);
|
||||
const button = rule(footer + " .btn");
|
||||
expect(button).toMatch(/height:\s*44px/);
|
||||
expect(button).toMatch(/border-radius:\s*10px/);
|
||||
expect(button).toMatch(/border-radius:\s*var\(--r-md\)/);
|
||||
});
|
||||
|
||||
it("puts refuse on the left in the error colour and allow on the right in the accent", () => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.msug-line > b { min-width: 0; overflow: hidden; color: var(--text); font-weight: var(--w-semi); text-overflow: ellipsis; white-space: nowrap; }
|
||||
.msug-meta { flex: none; color: var(--faint); font-size: var(--fs-meta); }
|
||||
.msug-acts { display: flex; align-items: center; gap: 6px; margin-inline-start: 22px; }
|
||||
.msug-acts > button { height: 28px; padding: 0 14px; border: 1px solid var(--border); border-radius: 7px; background: none; color: var(--text); font: var(--w-med) var(--fs-ui) var(--ui); cursor: pointer; }
|
||||
.msug-acts > button { height: 28px; padding: 0 14px; border: 1px solid var(--border); border-radius: var(--r-sm); background: none; color: var(--text); font: var(--w-med) var(--fs-ui) var(--ui); cursor: pointer; }
|
||||
.msug-acts > button[data-primary] { border-color: transparent; background: var(--accent); color: var(--on-accent, #fff); }
|
||||
.msug-acts > button[data-action="memory.suggest-dismiss"] { border-color: transparent; color: var(--muted); }
|
||||
.msug-acts > button:disabled { opacity: .5; cursor: default; }
|
||||
@@ -17,6 +17,6 @@
|
||||
.memproactive b { color: var(--text); font: var(--w-semi) var(--fs-ui) var(--ui); }
|
||||
.memproactive span { color: var(--faint); font: var(--w-med) var(--fs-meta)/1.5 var(--ui); }
|
||||
.memproactive-err { color: var(--danger, #c0392b) !important; }
|
||||
.memproactive-seg { display: inline-flex; flex: none; overflow: hidden; border: 1px solid var(--border); border-radius: 8px; }
|
||||
.memproactive-seg { display: inline-flex; flex: none; overflow: hidden; border: 1px solid var(--border); border-radius: var(--r-sm); }
|
||||
.memproactive-seg > button { padding: 6px 12px; border: 0; background: none; color: var(--muted); font: var(--w-med) var(--fs-meta) var(--ui); cursor: pointer; }
|
||||
.memproactive-seg > button[data-on] { background: var(--overlay); color: var(--text); }
|
||||
@@ -429,7 +429,6 @@ export const CHAT: Messages = {
|
||||
"chat.sort_by_price": { zh: "按价格排序", en: "Sort by price" },
|
||||
"chat.start_a_read_only_research": { zh: "派出一个只读的研究子代理做专项调查,它在隔离会话里只用只读工具。", en: "Start a read-only research sub-agent for a focused investigation, in an isolated session with read-only tools." },
|
||||
"chat.start_a_sub_agent_for": { zh: "派出一个子代理处理一项专项子任务,只返回它的最终答案。", en: "Start a sub-agent for a focused sub-task and return only its final answer." },
|
||||
"chat.started_at_worked_for_t": { zh: "{at} 开始 · 耗时 {t}", en: "Started {at} · worked for {t}" },
|
||||
"chat.starts_n_attempts_in_parallel": { zh: "并行启动 {n} 个候选", en: "starts {n} attempts in parallel" },
|
||||
"chat.starts_several_attempts_in_parallel": { zh: "并行启动多个候选", en: "starts several attempts in parallel" },
|
||||
"chat.static_html_preview": { zh: "HTML 静态预览", en: "Static HTML preview" },
|
||||
@@ -533,7 +532,6 @@ export const CHAT: Messages = {
|
||||
"chat.why_confirmation_is_needed": { zh: "需要确认的原因", en: "Why confirmation is needed" },
|
||||
"chat.withdrawn": { zh: "已撤回", en: "Withdrawn" },
|
||||
"chat.work_in_between": { zh: "中间过程", en: "Work in between" },
|
||||
"chat.worked_for_t": { zh: "耗时 {t}", en: "Worked for {t}" },
|
||||
"chat.write_and_save_a_new": { zh: "编写并保存一个新技能,以后的对话可以反复调用。", en: "Write and save a new skill that later conversations can invoke again." },
|
||||
"chat.write_file": { zh: "写入文件", en: "Write file" },
|
||||
"chat.write_full_file_content_overwriting": { zh: "写入完整的文件内容,覆盖原文件并创建所需目录;只改一部分请优先用 edit_file 或 multi_edit。", en: "Write full file content, overwriting the file and creating parent folders; prefer edit_file or multi_edit for a partial change." },
|
||||
|
||||
@@ -28,7 +28,6 @@ export const COMPOSER: Messages = {
|
||||
"composer.billed_at_the_vendors_published_4": { zh: "现在按供应商公布的高峰价计费,约 {left} 后恢复平峰价", en: "Billed at the vendor's published peak rate now; off-peak again in about {left}" },
|
||||
"composer.blocked": { zh: "受阻", en: "blocked" },
|
||||
"composer.branches_cannot_be_shown_for": { zh: "分支暂时无法显示。重新打开这个文件夹,或重启 Studio 后再试", en: "Branches cannot be shown for now. Reopen this folder, or restart Studio, and try again" },
|
||||
"composer.bytes": { zh: "字节", en: "Bytes" },
|
||||
"composer.cache": { zh: "缓存", en: "Cache" },
|
||||
"composer.checked_out_in_another_worktree": { zh: "已由其他工作树检出", en: "Checked out in another worktree" },
|
||||
"composer.choose_model": { zh: "选择模型", en: "Choose model" },
|
||||
@@ -52,7 +51,6 @@ export const COMPOSER: Messages = {
|
||||
"composer.could_not_add_retry": { zh: "添加失败 · 重试", en: "Could not add · Retry" },
|
||||
"composer.could_not_read_branches": { zh: "分支读取失败", en: "Could not read branches" },
|
||||
"composer.could_not_read_the_model": { zh: "读取模型列表失败", en: "Could not read the model list" },
|
||||
"composer.could_not_read_this_entry": { zh: "无法读取该条的正文,未打开编辑:{why}", en: "Could not read this entry back, so it was not opened for editing: {why}" },
|
||||
"composer.create_an_isolated_worktree_when": { zh: "发送消息时创建独立工作树,基于所选分支的已提交版本,不包含未提交修改;端口、数据库等运行资源仍可能共享。", en: "Create an isolated worktree when you send the message, using the selected branch's committed version without uncommitted changes; ports, databases, and other runtime resources may still be shared." },
|
||||
"composer.create_from_the_committed_version": { zh: "从已提交版本创建,任务和附件将在就绪后发送", en: "Create from the committed version; the task and attachments are sent when ready" },
|
||||
"composer.credentials_were_rejected_availability_is": { zh: "凭据未通过,尚未确认", en: "Credentials were rejected; availability is still unknown" },
|
||||
@@ -74,7 +72,6 @@ export const COMPOSER: Messages = {
|
||||
"composer.display_alias_for_name": { zh: "{name} 的显示别名", en: "Display alias for {name}" },
|
||||
"composer.display_only_the_model_id": { zh: "仅用于显示,不改变请求中的模型 ID", en: "Display only; the model ID sent in requests is unchanged" },
|
||||
"composer.dont_show_again_this_session": { zh: "不再提示(本会话)", en: "Don’t show again (this session)" },
|
||||
"composer.edit": { zh: "改", en: "Edit" },
|
||||
"composer.effort_desc.auto": { zh: "使用模型默认或自适应策略,按任务复杂度调整", en: "Use the model default or adapt to task complexity" },
|
||||
"composer.effort_desc.disabled": { zh: "不发送推理强度参数,使用服务端默认设置", en: "Do not send an effort parameter; use the server default" },
|
||||
"composer.effort_desc.high": { zh: "投入更多时间分析复杂上下文与执行方案", en: "Spend more time analyzing complex context and execution plans" },
|
||||
@@ -105,7 +102,6 @@ export const COMPOSER: Messages = {
|
||||
"composer.has_an_image_sent_when": { zh: "含图片,本轮结束后发送", en: "Has an image: sent when this turn ends" },
|
||||
"composer.hide_amounts": { zh: "隐藏金额", en: "Hide amounts" },
|
||||
"composer.hit_hit_miss_miss": { zh: "命中 {hit} · 未命中 {miss}", en: "Hit {hit} · Miss {miss}" },
|
||||
"composer.hold": { zh: "暂停派发", en: "Hold" },
|
||||
"composer.how_long_this_step_has": { zh: "这一步已经跑了多久", en: "How long this step has been running" },
|
||||
"composer.image": { zh: "图片", en: "image" },
|
||||
"composer.image_capability_is_unverified_click": { zh: "图片能力未确认;点击后按手动声明启用", en: "Image capability is unverified; click to enable it as a manual declaration" },
|
||||
@@ -114,7 +110,6 @@ export const COMPOSER: Messages = {
|
||||
"composer.images": { zh: "读图?", en: "Images?" },
|
||||
"composer.in_use_now": { zh: "现在用了", en: "In use now" },
|
||||
"composer.isolated_worktree_branch": { zh: "独立工作树 · {branch}", en: "Isolated worktree · {branch}" },
|
||||
"composer.items": { zh: "条目", en: "Items" },
|
||||
"composer.keep_typing": { zh: "继续输入…", en: "Keep typing…" },
|
||||
"composer.keep_typing_to_queue_further": { zh: "继续输入以排队后续修改", en: "Keep typing to queue further changes" },
|
||||
"composer.key_to_queue_enter_for": { zh: "{key} 排队 · Enter 换行", en: "{key} to queue · Enter for a new line" },
|
||||
@@ -141,13 +136,10 @@ export const COMPOSER: Messages = {
|
||||
"composer.n_characters_lines_lines": { zh: "{n} 字 · {lines} 行", en: "{n} characters · {lines} lines" },
|
||||
"composer.n_d": { zh: "{n} 天", en: "{n} d" },
|
||||
"composer.n_files_changed": { zh: "{n} 个文件已更改", en: "{n} files changed" },
|
||||
"composer.n_frozen": { zh: "冻结 {n} 文件", en: "{n} frozen" },
|
||||
"composer.n_lines_put_it_back": { zh: "{n} 行 · 展开到输入框", en: "{n} lines · put it back in the box" },
|
||||
"composer.n_more_not_listed_search": { zh: "另有 {n} 个未列出,可通过搜索找到", en: "{n} more not listed — search to reach them" },
|
||||
"composer.n_more_waiting_for_your": { zh: "还有 {n} 条等你决定", en: "{n} more waiting for your decision" },
|
||||
"composer.n_not_verified": { zh: "{n} 个未验证", en: "{n} not verified" },
|
||||
"composer.n_recovered": { zh: "恢复了 {n} 条", en: "{n} recovered" },
|
||||
"composer.n_to_send": { zh: "{n} 条待发送", en: "{n} to send" },
|
||||
"composer.n_tokens_in_all": { zh: "共 {n} tokens", en: "{n} tokens in all" },
|
||||
"composer.n_unavailable": { zh: "{n} 个不可用", en: "{n} unavailable" },
|
||||
"composer.n_undetermined": { zh: "{n} 个无法确定", en: "{n} undetermined" },
|
||||
@@ -167,7 +159,6 @@ export const COMPOSER: Messages = {
|
||||
"composer.no_os_sandbox_is_in": { zh: "当前没有生效的系统沙盒(Windows 暂不支持,或已在设置中关闭),命令的写入不受限制,所以编辑和命令执行前都会先问你。切换到自动批准后,之后所有文件夹的新会话都会使用它。", en: "No OS sandbox is in effect (Windows has none yet, or it is turned off in Settings), so nothing limits what commands write - edits and commands ask you first. Switching to auto-approve applies to new sessions in every folder." },
|
||||
"composer.no_reasoning": { zh: "不思考", en: "No reasoning" },
|
||||
"composer.nobody_has_said_whether_this": { zh: "该模型是否支持读图未声明 · 暂按不支持处理;在「连接」中勾选后即可直接发送", en: "Nobody has said whether this model reads images · treated as no for now; tick it under Connection to send them straight through" },
|
||||
"composer.not_delivered_yet_edit_it": { zh: "尚未送达,可以改,也可以取回", en: "Not delivered yet — edit it or take it back" },
|
||||
"composer.not_listed_read_available_models": { zh: "没找到?从服务商读取可用模型", en: "Not listed? Read available models from provider" },
|
||||
"composer.no_sandbox_ask_first": { zh: "没有系统沙盒,执行前都会先问你", en: "No OS sandbox, so commands ask first" },
|
||||
"composer.not_now": { zh: "暂不信任", en: "Not now" },
|
||||
@@ -191,12 +182,10 @@ export const COMPOSER: Messages = {
|
||||
"composer.pro_mode": { zh: "Pro 模式", en: "Pro mode" },
|
||||
"composer.providers_api_access_and_available": { zh: "服务商、API 与可用模型", en: "Providers, API access, and available models" },
|
||||
"composer.queue": { zh: "排队发送", en: "Queue" },
|
||||
"composer.queue_capacity": { zh: "队列容量", en: "Queue capacity" },
|
||||
"composer.quote_name": { zh: "引用 {name}", en: "Quote {name}" },
|
||||
"composer.quoting_a_reply": { zh: "引用回复", en: "quoting a reply" },
|
||||
"composer.quoting_turn_n": { zh: "引用第 {n} 轮回复", en: "quoting turn {n}" },
|
||||
"composer.rate_limited_availability_is_still": { zh: "遇到限流,尚未确认", en: "Rate limited; availability is still unknown" },
|
||||
"composer.re_freeze_the_files_it": { zh: "重新冻结引用的文件", en: "Re-freeze the files it quotes" },
|
||||
"composer.read_only_not_in_the": { zh: "只读 · 不写入主会话 · 关闭即丢", en: "Read-only · not in the main session · gone when closed" },
|
||||
"composer.reading_branches": { zh: "正在读取分支…", en: "Reading branches…" },
|
||||
"composer.reasoning_effort": { zh: "推理强度", en: "Reasoning effort" },
|
||||
@@ -243,8 +232,6 @@ export const COMPOSER: Messages = {
|
||||
"composer.send_now": { zh: "立即发送", en: "Send now" },
|
||||
"composer.send_shortcut": { zh: "发送快捷键", en: "Send shortcut" },
|
||||
"composer.send_with_enter_or_with": { zh: "Enter 发送还是 Ctrl+Enter 发送", en: "Send with Enter or with Ctrl+Enter" },
|
||||
"composer.sending": { zh: "正在送出…", en: "Sending…" },
|
||||
"composer.sending_is_paused": { zh: "发送已暂停", en: "Sending is paused" },
|
||||
"composer.server_returned_http_status": { zh: "服务器返回 HTTP {status}", en: "Server returned HTTP {status}" },
|
||||
"composer.session_messages_waiting_for_your": { zh: "等你决定的会话间消息", en: "Session messages waiting for your decision" },
|
||||
"composer.show_amounts": { zh: "显示金额", en: "Show amounts" },
|
||||
@@ -293,7 +280,6 @@ export const COMPOSER: Messages = {
|
||||
"composer.thinking": { zh: "思考中", en: "Thinking" },
|
||||
"composer.thinking_stays_on_and_is": { zh: "思考仍开启并计费,该模型无法关闭思考", en: "Thinking stays on and is billed; this model cannot turn it off" },
|
||||
"composer.this_connection_will_not_send": { zh: "当前连接不会向这个模型发送图片", en: "This connection will not send images to this model" },
|
||||
"composer.this_conversation_is_temporarily_read": { zh: "会话暂时只读,消息已为你保留", en: "This conversation is temporarily read-only; your message is preserved" },
|
||||
"composer.this_endpoint_reported_no_reasoning": { zh: "这个端点没有报告推理档位,中转站通常不转发这项能力。将打开该来源的编辑表单,可以选择思考参数或直接填写档位。", en: "This endpoint reported no reasoning levels; relays often do not forward them. Opens this source’s editor, where you can pick a thinking parameter or list the levels yourself." },
|
||||
"composer.this_folder_has_a_git": { zh: "这个文件夹有 Git 仓库,但 Git 拒绝打开它", en: "This folder has a Git repository, but Git refused to open it" },
|
||||
"composer.this_folder_has_a_git_2": { zh: "这个文件夹有 Git 仓库,但 Git 没有及时响应", en: "This folder has a Git repository, but Git did not respond in time" },
|
||||
|
||||
@@ -216,7 +216,6 @@ export const EXT: Messages = {
|
||||
"ext.not_pinned": { zh: "未固定", en: "Not pinned" },
|
||||
"ext.not_previewed": { zh: "未预览", en: "Not previewed" },
|
||||
"ext.not_running": { zh: "未运行", en: "Not running" },
|
||||
"ext.not_the_project_this_session": { zh: "不是当前会话所在的项目 —— 只能改开关", en: "Not the project this session is in — only the switches can change" },
|
||||
"ext.nothing_is_connected_right_now": { zh: "当前未连接,以下是上次连接时它返回的内容。", en: "Nothing is connected right now; this is what the service itself answered the last time it was." },
|
||||
"ext.nothing_new_that_runs": { zh: "没有新增的可执行内容。", en: "Nothing new that runs." },
|
||||
"ext.nothing_published_yet": { zh: "还没有发布过。", en: "Nothing published yet." },
|
||||
|
||||
@@ -6,7 +6,6 @@ export const LOOK: Messages = {
|
||||
"look.at_the_current_window_size": { zh: "当前窗口尺寸下,此壁纸只有上下会被裁剪,左右正好填满,因此横向焦点无法调整。", en: "At the current window size this wallpaper is cropped only top and bottom and fills the width exactly, so the horizontal focus cannot be moved." },
|
||||
"look.at_the_current_window_size_2": { zh: "当前窗口尺寸下,此壁纸只有左右会被裁剪,上下正好填满,因此纵向焦点无法调整。", en: "At the current window size this wallpaper is cropped only left and right and fills the height exactly, so the vertical focus cannot be moved." },
|
||||
"look.back_to_the_default_typeface": { zh: "恢复默认字体", en: "Back to the default typeface" },
|
||||
"look.background_blur_the_animated_sky": { zh: "背景模糊、动态天空与颗粒质感全部开启", en: "Background blur, the animated sky and the grain layer are all on" },
|
||||
"look.between_the_two": { zh: "介于两者之间", en: "Between the two" },
|
||||
"look.body_line_width": { zh: "正文行宽", en: "Body line width" },
|
||||
"look.body_text_size": { zh: "正文字号", en: "Body text size" },
|
||||
@@ -70,7 +69,6 @@ export const LOOK: Messages = {
|
||||
"look.prose_and_the_composer_both": { zh: "正文和输入框都跟随窗口宽度;输入框很宽时一行不易回到行首", en: "Prose and the composer both follow the window; a very wide composer makes a line harder to find the start of" },
|
||||
"look.prose_follows_the_window_with": { zh: "正文跟随窗口宽度,宽屏上不留两侧空白", en: "Prose follows the window, with nothing left empty at the sides" },
|
||||
"look.reading": { zh: "适宜阅读", en: "Reading" },
|
||||
"look.reduced": { zh: "节能", en: "Reduced" },
|
||||
"look.regular": { zh: "常规", en: "Regular" },
|
||||
"look.saved_quit_and_reopen_studio": { zh: "已保存,退出并重新打开 Studio 后生效", en: "Saved; quit and reopen Studio for it to apply" },
|
||||
"look.scheduled": { zh: "定时", en: "Scheduled" },
|
||||
@@ -96,7 +94,6 @@ export const LOOK: Messages = {
|
||||
"look.third_party": { zh: "第三方", en: "Third party" },
|
||||
"look.this_launch_uses_mode": { zh: "当前启动使用:{mode}", en: "This launch uses: {mode}" },
|
||||
"look.this_wallpaper_matches_the_windows": { zh: "此壁纸与窗口宽高比一致,四边均未裁剪;改变窗口尺寸后才可调整焦点。", en: "This wallpaper matches the window's aspect ratio and is not cropped on any side; the focus becomes adjustable once the window changes shape." },
|
||||
"look.turns_off_background_blur_the": { zh: "关闭背景模糊、动态天空与颗粒质感,集成显卡或老旧电脑上更省电", en: "Turns off background blur, the animated sky and the grain layer; lighter on integrated graphics and older machines" },
|
||||
"look.typeface": { zh: "字体", en: "Typeface" },
|
||||
"look.typeface_preview_aa_bb_0123": { zh: "字体预览 · 中文 Aa Bb 0123", en: "Typeface preview · Aa Bb 0123" },
|
||||
"look.unmute_sound": { zh: "打开声音", en: "Unmute sound" },
|
||||
|
||||
@@ -16,7 +16,6 @@ export const PLAN: Messages = {
|
||||
"plan.goal.last_reason": { zh: "最近一次判断:{reason}", en: "Latest assessment: {reason}" },
|
||||
"plan.goal.pause": { zh: "暂停", en: "Pause" },
|
||||
"plan.goal.resume": { zh: "继续", en: "Resume" },
|
||||
"plan.goal.stop_cause": { zh: "停下的原因:{cause}", en: "Why it stopped: {cause}" },
|
||||
"plan.goal.usage": { zh: "目标用量", en: "Goal usage" },
|
||||
"plan.goal.usage_line": { zh: "{requests} 次请求 · {tokens} tokens · 用时 {time}", en: "{requests} requests · {tokens} tokens · {time}" },
|
||||
"plan.now": { zh: "正在做:{step}", en: "Working on: {step}" },
|
||||
|
||||
@@ -132,9 +132,7 @@ export const SETTINGS: Messages = {
|
||||
"settings.remote": { zh: "远程", en: "Remote" },
|
||||
"settings.reset_arrangement": { zh: "还原排列", en: "Reset arrangement" },
|
||||
"settings.rules_you_spell_out": { zh: "明确的规则", en: "Rules you spell out" },
|
||||
"settings.run_your_own_commands_before": { zh: "在 agent 执行任务前后运行自定义命令。这些命令在本机以当前用户权限运行;可拦截 agent 的两个事件已在下方标出。", en: "Run your own commands before and after the agent works. They run on this machine with your permissions; the two events that can block the agent are marked below." },
|
||||
"settings.running_or_waiting_on_you": { zh: "通过图标即可判断正在运行还是等待批准", en: "Running, or waiting on you — one glance at the icon says which" },
|
||||
"settings.runtime": { zh: "运行时", en: "Runtime" },
|
||||
"settings.sandbox": { zh: "沙箱", en: "Sandbox" },
|
||||
"settings.sandbox_and_execution": { zh: "沙箱与执行", en: "Sandbox and execution" },
|
||||
"settings.scheduled_runs_hint": { zh: "到点让 Agent 只读地跑一遍并留下报告", en: "Have the agent run a read-only pass on a schedule and leave a report" },
|
||||
|
||||
@@ -12,7 +12,8 @@ const REGISTERED: Record<string, string> = {
|
||||
"onb-mark-in": "Onb-Motion", "onb-rise": "Onb-Motion", "onb-breathe": "Onb-Motion", "onb-grid": "Onb-Motion", "onb-banner-in": "Onb-Motion", "onb-dash": "Onb-Motion", "onb-land": "Onb-Motion", "onb-leave": "Onb-Motion", "compose-rise": "Onb-Motion",
|
||||
"onb-step-in": "Onb-Motion", "onb-step-back": "Onb-Motion", "onb-tile": "Onb-Motion", "onb-shake": "Onb-Motion",
|
||||
oobebrand: "Onb-Flow", oobecollapse: "Onb-Flow", oobefill: "Onb-Flow", oobegone: "Onb-Flow", oobelume: "Onb-Flow", oobeout: "Onb-Flow", oobesay: "Onb-Flow", oobeshrink: "Onb-Flow",
|
||||
"plan-spin-stem": "Chat-Plan", "plan-spin-bowl": "Chat-Plan", "plan-spin-leg": "Chat-Plan",
|
||||
"plan-spin-stem": "Chat-Plan", "plan-spin-bowl": "Chat-Plan", "plan-spin-leg": "Chat-Plan", "plan-open": "Chat-Plan", "plan-close": "Chat-Plan",
|
||||
"es-flow": "Chat-Composer", "sheet-up": "Chat-Composer", "ph-run": "Run-Phases",
|
||||
"workbench-tab-in": "WB-Frame", "workbench-live": "WB-Frame",
|
||||
"srail-flash": "Sess-States", "srail-peek": "Sess-States", rowsweep: "Sess-States", "agent-live": "Subagent",
|
||||
"sel-tools-in": "Chat-Reply", "sel-tools-under": "Chat-Reply",
|
||||
|
||||
@@ -32,12 +32,12 @@ const CONSENT_LABEL: Record<string, string> = {
|
||||
// in the page, in one strip per outcome.
|
||||
function RestoreDialog({ title, onClose, footer, wide, children }: { title: string; onClose: () => void; footer: ReactNode; wide?: boolean; children: ReactNode }) {
|
||||
return createPortal(
|
||||
<div className="ns-veil" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
|
||||
<div className="ns-dlg bk-dlg" data-wide={wide ? "" : undefined} role="dialog" aria-modal="true" aria-label={title}
|
||||
<div className="ns-veil" data-action-mousedown="layer.dismiss" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
|
||||
<div className="ns-dlg bk-dlg" data-wide={wide ? "" : undefined} role="dialog" aria-modal="true" aria-label={title} data-action-keydown="layer.dismiss"
|
||||
onKeyDown={(e) => { if (e.key === "Escape") { e.stopPropagation(); onClose(); } }}>
|
||||
<header>
|
||||
<h3>{title}</h3>
|
||||
<button type="button" className="ns-x" aria-label={t("common.close")} title={t("common.close")} onClick={onClose}><StudioIcon name="close" /></button>
|
||||
<button type="button" className="ns-x" data-action="layer.dismiss" aria-label={t("common.close")} title={t("common.close")} onClick={onClose}><StudioIcon name="close" /></button>
|
||||
</header>
|
||||
<div className="bk-dlg-body">{children}</div>
|
||||
<footer>{footer}</footer>
|
||||
@@ -162,7 +162,7 @@ export function BackupRestore({ port, source, onClose }: { port: AgentPort; sour
|
||||
<div className="bk-fields" data-one="">
|
||||
<label className="grow">
|
||||
<span>{t("data.passphrase")}</span>
|
||||
<input autoFocus data-action="backup.passphrase" data-target="open" type="password" autoComplete="current-password" disabled={busy} value={pass} onChange={(e) => setPass(e.target.value)}
|
||||
<input autoFocus data-action-change="backup.passphrase" data-action-keydown="backup.restore" data-target="open" type="password" autoComplete="current-password" disabled={busy} value={pass} onChange={(e) => setPass(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" && pass && !busy) void open(); }} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -60,6 +60,12 @@ export function EffortSlider({ name, model, levels, current, onPick }: {
|
||||
<input
|
||||
type="range" min={0} max={last} step="any" value={pos}
|
||||
aria-label={name} aria-valuetext={levels[near].label}
|
||||
data-action-change="reasoning.effort-preview"
|
||||
data-action-pointerdown="reasoning.effort-preview"
|
||||
data-action-pointerup="reasoning.effort"
|
||||
data-action-pointercancel="reasoning.effort"
|
||||
data-action-keydown="reasoning.effort-preview"
|
||||
data-action-keyup="reasoning.effort"
|
||||
onChange={(e) => setValue(Number(e.target.value))}
|
||||
onPointerDown={() => setHeld(true)}
|
||||
onPointerUp={release} onPointerCancel={release}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { clampedShare } from "./meterShare";
|
||||
|
||||
// One horizontal bar for a quantity Reasonix owns that has a natural maximum.
|
||||
// The fill is the share of max, shown in the full-state colour once the value
|
||||
// reaches it; the accessible name and numbers say what the bar measures.
|
||||
export function Meter({ value, max, label, full = false }: { value: number; max: number; label: string; full?: boolean }) {
|
||||
return (
|
||||
<span className="mtr" role="meter" aria-label={label} aria-valuemin={0} aria-valuemax={max} aria-valuenow={Math.min(value, max)} data-full={full || undefined}>
|
||||
<i style={{ width: `${clampedShare(value, max)}%` }} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -40,15 +40,15 @@ export function NativeSessionImport({ hub, workspace, onImported }: { hub: HubPo
|
||||
{message && <div className="ns-done" role="status"><StudioIcon name="check" />{message}</div>}
|
||||
{error && <div className="ns-fail" role="alert"><StudioIcon name="warning" />{error}</div>}
|
||||
{asking && createPortal(
|
||||
<div className="ns-veil" onMouseDown={(e) => { if (e.target === e.currentTarget) setAsking(false); }}>
|
||||
<div className="ns-dlg" role="dialog" aria-modal="true" aria-labelledby="ns-title"
|
||||
<div className="ns-veil" data-action-mousedown="layer.dismiss" onMouseDown={(e) => { if (e.target === e.currentTarget) setAsking(false); }}>
|
||||
<div className="ns-dlg" role="dialog" aria-modal="true" aria-labelledby="ns-title" data-action-keydown="layer.dismiss"
|
||||
onKeyDown={(e) => { if (e.key === "Escape") { e.stopPropagation(); setAsking(false); } }}>
|
||||
<header>
|
||||
<h3 id="ns-title">{t("data.import_a_native_session")}</h3>
|
||||
<button type="button" className="ns-x" aria-label={t("common.close")} title={t("common.close")} onClick={() => setAsking(false)}><StudioIcon name="close" /></button>
|
||||
<button type="button" className="ns-x" data-action="layer.dismiss" aria-label={t("common.close")} title={t("common.close")} onClick={() => setAsking(false)}><StudioIcon name="close" /></button>
|
||||
</header>
|
||||
<p>{t("data.keep_the_composition_version_and")}</p>
|
||||
<div className="ns-drop" data-over={over ? "" : undefined}
|
||||
<div className="ns-drop" data-over={over ? "" : undefined} data-action-dragover="session.import-native-choose" data-action-dragleave="session.import-native-choose" data-action-drop="session.import-native-choose"
|
||||
onDragOver={(e) => { e.preventDefault(); setOver(true); }}
|
||||
onDragLeave={() => setOver(false)}
|
||||
onDrop={(e) => { e.preventDefault(); setOver(false); take(e.dataTransfer.files?.[0]); }}>
|
||||
@@ -56,7 +56,7 @@ export function NativeSessionImport({ hub, workspace, onImported }: { hub: HubPo
|
||||
<span>{t("data.drop_a_session_json_file_here")}</span>
|
||||
<button type="button" className="btn" autoFocus data-action="session.import-native-choose" onClick={() => input.current?.click()}>{t("data.choose_a_session_json_file")}</button>
|
||||
</div>
|
||||
<footer><button type="button" className="ns-cancel" onClick={() => setAsking(false)}>{t("common.cancel")}</button></footer>
|
||||
<footer><button type="button" className="ns-cancel" data-action="layer.dismiss" onClick={() => setAsking(false)}>{t("common.cancel")}</button></footer>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { pinToViewport } from "./place";
|
||||
|
||||
// Gap between the text and the bubble, in the space a rect reports. It lives
|
||||
// here rather than in CSS for the reason Context's does: a fixed element is
|
||||
// placed by measurement, so no rule owns the offset any more.
|
||||
const GAP = 8;
|
||||
// Long enough that reading down a list does not open one bubble per row.
|
||||
const OPEN_MS = 420;
|
||||
// The pointer has to be able to cross the gap into the bubble to select from it.
|
||||
const SHUT_MS = 140;
|
||||
|
||||
// One observer for every clipped-or-not question, not one per row: a queue at
|
||||
// its ceiling is 64 of these on screen at once, and the transcript already
|
||||
// settled that a shared observer is what the API is for.
|
||||
const asked = new Map<Element, (clipped: boolean) => void>();
|
||||
let lens: ResizeObserver | null = null;
|
||||
|
||||
function watch(el: Element, cb: (clipped: boolean) => void) {
|
||||
asked.set(el, cb);
|
||||
lens ??= new ResizeObserver((entries) => {
|
||||
for (const e of entries) asked.get(e.target)?.(clipped(e.target));
|
||||
});
|
||||
lens.observe(el);
|
||||
cb(clipped(el));
|
||||
return () => {
|
||||
asked.delete(el);
|
||||
lens?.unobserve(el);
|
||||
};
|
||||
}
|
||||
|
||||
// Rounded up on both sides: sub-pixel layout reports a one-pixel difference on
|
||||
// text that is not actually cut, and a bubble offering to show you what you can
|
||||
// already read is worse than none.
|
||||
const clipped = (el: Element) => el.scrollWidth > el.clientWidth + 1;
|
||||
|
||||
/** Overflow shows the whole of a single-line, ellipsised string — and only when
|
||||
* there is more of it than fits. It replaces the native title attribute, whose
|
||||
* delay, placement, size and scrollability all belong to the browser: a long
|
||||
* one is an uncontrollable sheet over the page. This one is bounded, scrolls
|
||||
* inside itself, and lets the text be selected. */
|
||||
export function Overflow({ text, className }: { text: string; className?: string }) {
|
||||
const anchor = useRef<HTMLSpanElement>(null);
|
||||
const [cut, setCut] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const timer = useRef(0);
|
||||
// Set while the pointer is over the bubble, so leaving the text does not shut
|
||||
// the thing the pointer is on its way to.
|
||||
const held = useRef(false);
|
||||
|
||||
useEffect(() => (anchor.current ? watch(anchor.current, setCut) : undefined), [text]);
|
||||
|
||||
const after = useCallback((ms: number, to: boolean) => {
|
||||
clearTimeout(timer.current);
|
||||
timer.current = window.setTimeout(() => {
|
||||
if (to && !clipped(anchor.current!)) return;
|
||||
setOpen(to);
|
||||
}, ms);
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => clearTimeout(timer.current), []);
|
||||
|
||||
// Placed once, and stale the moment anything moves it. There is nothing
|
||||
// useful to show mid-scroll, so it closes rather than chasing.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const shut = () => setOpen(false);
|
||||
// Capture, and stopped there — the convention useDismiss set. A transient
|
||||
// surface owns Escape while it is up, and the window's own handler (which
|
||||
// stops a turn, or leaves focus) must not also act on the same press.
|
||||
const key = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Escape") return;
|
||||
e.stopPropagation();
|
||||
shut();
|
||||
};
|
||||
addEventListener("scroll", shut, true);
|
||||
addEventListener("resize", shut);
|
||||
addEventListener("keydown", key, true);
|
||||
return () => {
|
||||
removeEventListener("scroll", shut, true);
|
||||
removeEventListener("resize", shut);
|
||||
removeEventListener("keydown", key, true);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const place = (el: HTMLDivElement | null) => {
|
||||
if (!el || !anchor.current) return;
|
||||
const to = anchor.current.getBoundingClientRect();
|
||||
const box = el.getBoundingClientRect();
|
||||
const above = to.top - box.height - GAP;
|
||||
pinToViewport(el, to.left, above >= 6 ? above : to.bottom + GAP);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
ref={anchor}
|
||||
className={className}
|
||||
// A tab stop only where there is something behind it. Sixty-four rows
|
||||
// that all take focus and none of which has more to say is a keyboard
|
||||
// path through nothing.
|
||||
tabIndex={cut ? 0 : undefined}
|
||||
data-cut={cut ? "" : undefined}
|
||||
onPointerEnter={() => after(OPEN_MS, true)}
|
||||
onPointerLeave={() => !held.current && after(SHUT_MS, false)}
|
||||
onFocus={() => after(OPEN_MS, true)}
|
||||
onBlur={() => after(0, false)}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
{open &&
|
||||
// To body, the way the rewind menu goes. A finished entry animation with
|
||||
// fill-mode: both leaves transform as the identity matrix rather than
|
||||
// none, and an identity matrix is still a containing block for a fixed
|
||||
// child — measured here as a bubble whose inline left/top were right and
|
||||
// which rendered 675px below the window. Every row in this queue carries
|
||||
// one, so nothing anchored inside a card may position against the
|
||||
// viewport from where it sits.
|
||||
createPortal(
|
||||
<div
|
||||
className="ovf"
|
||||
role="tooltip"
|
||||
ref={place}
|
||||
onPointerEnter={() => {
|
||||
held.current = true;
|
||||
clearTimeout(timer.current);
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
held.current = false;
|
||||
after(SHUT_MS, false);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { arrowTabs } from "./tablist";
|
||||
import { pagesOf } from "./settingPages";
|
||||
import { UpdateDot } from "./UpdateDot";
|
||||
import { useFolded } from "./usefold";
|
||||
import { listenAction } from "./listen";
|
||||
|
||||
interface Props {
|
||||
at: Section;
|
||||
@@ -25,13 +26,13 @@ export function SettingsNav({ at, values, shown, danger, disabled, onSelect, pag
|
||||
useEffect(() => {
|
||||
const nav = strip.current;
|
||||
if (!phone || !nav) return;
|
||||
const sideways = (e: WheelEvent) => {
|
||||
if (e.deltaX !== 0 || e.deltaY === 0) return;
|
||||
nav.scrollLeft += e.deltaY;
|
||||
e.preventDefault();
|
||||
const sideways = (e: Event) => {
|
||||
const wheel = e as WheelEvent;
|
||||
if (wheel.deltaX !== 0 || wheel.deltaY === 0) return;
|
||||
nav.scrollLeft += wheel.deltaY;
|
||||
wheel.preventDefault();
|
||||
};
|
||||
nav.addEventListener("wheel", sideways, { passive: false });
|
||||
return () => nav.removeEventListener("wheel", sideways);
|
||||
return listenAction(nav, "wheel", { action: "settings.sections-scroll", listener: sideways }, { passive: false });
|
||||
}, [phone]);
|
||||
useEffect(() => {
|
||||
if (!phone) return;
|
||||
|
||||
@@ -302,6 +302,7 @@ export const SETTINGS: SettingEntry[] = [
|
||||
{ section: "appearance", anchor: "contrast", title: "look.text_contrast", scope: "machine", apply: "immediate", keywords: ["柔和", "对比"] },
|
||||
{ section: "appearance", anchor: "effects", title: "look.visual_effects", scope: "machine", apply: "immediate", keywords: ["磨砂", "模糊", "动画", "显卡", "gpu", "性能", "省电", "blur"] },
|
||||
{ section: "common", anchor: "mode", title: "settings.light_and_dark", scope: "machine", apply: "immediate", keywords: ["深色", "浅色", "跟随系统", "定时", "时间段", "schedule", "dark", "light"] },
|
||||
{ section: "appearance", anchor: "advanced-look", title: "look.advanced_look", scope: "machine", apply: "immediate", keywords: ["高级外观", "导入主题", "自定义颜色", "theme", "colours"] },
|
||||
{ section: "appearance", anchor: "scheme", page: true, title: "settings.palette", scope: "machine", apply: "immediate", keywords: ["主题", "theme", "色板"] },
|
||||
];
|
||||
|
||||
|
||||
@@ -24,15 +24,23 @@ function namesIn(n: Node): Set<string> {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Functions this file declares, by the name they are reached through. */
|
||||
/** Functions this file declares, by the name they are reached through. A
|
||||
* callback handed to a child as a prop is reached through the prop's name, so
|
||||
* a row component that calls onTake still leads to the take() it was given. */
|
||||
function localsOf(t: Node): Map<string, Node> {
|
||||
const out = new Map<string, Node>();
|
||||
const handedDown = new Map<string, Node>();
|
||||
walk(t, (n) => {
|
||||
if (n.type === "FunctionDeclaration" && (n.id as Node)?.name) out.set((n.id as Node).name as string, n);
|
||||
if (n.type === "VariableDeclarator" && (n.id as Node)?.type === "Identifier" && n.init) {
|
||||
out.set((n.id as Node).name as string, n.init as Node);
|
||||
}
|
||||
if (n.type === "JSXAttribute" && typeof (n.name as Node)?.name === "string") {
|
||||
const v = n.value as Node | undefined;
|
||||
if (v?.type === "JSXExpressionContainer") handedDown.set((n.name as Node).name as string, v.expression as Node);
|
||||
}
|
||||
});
|
||||
for (const [name, body] of handedDown) if (!out.has(name)) out.set(name, body);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ export interface Primitive {
|
||||
* prop absent here is not an input: a primitive forwards what it declares
|
||||
* and nothing else. */
|
||||
callbacks: Record<string, string>;
|
||||
/** The prop that names the action at a call site, when the component writes
|
||||
* it into data-action itself: a call site cannot spell data-action for an
|
||||
* element it does not render. */
|
||||
actionProp?: string;
|
||||
}
|
||||
|
||||
export const PRIMITIVES: Primitive[] = [
|
||||
@@ -48,6 +52,7 @@ export const PRIMITIVES: Primitive[] = [
|
||||
{ file: "ui/Switch.tsx", callbacks: { onClick: "click" } },
|
||||
{ file: "ui/Menu.tsx", callbacks: { onPick: "click" } },
|
||||
{ file: "ui/kit/Button.tsx", callbacks: { onClick: "click" } },
|
||||
{ file: "ui/SettingRow.tsx", callbacks: { onPick: "click", onClick: "click" }, actionProp: "action" },
|
||||
];
|
||||
|
||||
export const LIBRARY_PRIMITIVES: { module: string; name: string; callbacks: Record<string, string> }[] = [
|
||||
@@ -555,7 +560,7 @@ export function certifyRoots(sources: Map<string, string | Node>, opts: Options
|
||||
let abstraction: "host" | "primitive" | "wiring" | "unknown" = "host";
|
||||
// Set only for a certified primitive: the contract that says which of
|
||||
// its props are inputs, so this does not fall back to reading names.
|
||||
let contract: { callbacks: Record<string, string> } | undefined;
|
||||
let contract: { callbacks: Record<string, string>; actionProp?: string } | undefined;
|
||||
if (/^[A-Z]/.test(el)) {
|
||||
const imp = importsOf.get(path)?.get(el);
|
||||
const decl = imp?.file ?? (declaresLocally(trees.get(path)!, imp?.name ?? el) ? path : null);
|
||||
@@ -587,7 +592,8 @@ export function certifyRoots(sources: Map<string, string | Node>, opts: Options
|
||||
(abstraction === "wiring" ? nonUser : uncertified).push(note);
|
||||
return;
|
||||
}
|
||||
const generic = "data-action" in a ? allStrings(a["data-action"]) : null;
|
||||
const generic = "data-action" in a ? allStrings(a["data-action"])
|
||||
: contract?.actionProp && contract.actionProp in a ? allStrings(a[contract.actionProp]) : null;
|
||||
if (generic && handlers.length > 1) {
|
||||
refused.push({ path, line: n.loc!.start.line, why: "AMBIGUOUS_ELEMENT_ACTION", detail: handlers.map((h) => h.name).join("+") });
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SETTING_SCREEN, settingScreen, unknownScreens } from "./settingScreens"
|
||||
|
||||
describe("where the settings sheet draws a setting the model names", () => {
|
||||
it("opens the file delivery switch for its canonical setting id", () => {
|
||||
expect(settingScreen("tools.present_files")).toBe("tools:present-files");
|
||||
expect(settingScreen("tools.present_files")).toBe("display:present-files");
|
||||
});
|
||||
it("only names rows the sheet really has", () => {
|
||||
expect(unknownScreens()).toEqual([]);
|
||||
|
||||
@@ -11,7 +11,7 @@ export const SETTING_SCREEN: Record<string, string> = {
|
||||
"peers.dispatch": "peers:peers",
|
||||
"peers.release_projects": "peers:peer-projects",
|
||||
"desktop.default_tool_approval_mode": "tools:approval",
|
||||
"agent.plan_approval": "tools:plan-approval",
|
||||
"agent.plan_approval": "session:plan-approval",
|
||||
"agent.default_mode": "common:default-mode",
|
||||
"permissions.mode": "tools:approval",
|
||||
"tools.disabled": "tools:tool-list",
|
||||
@@ -27,24 +27,24 @@ export const SETTING_SCREEN: Record<string, string> = {
|
||||
"tools.shell.prefer": "tools:shell",
|
||||
"tools.shell.path": "tools:shell",
|
||||
"tools.browser_tools": "tools:browser-tools",
|
||||
"tools.present_files": "tools:present-files",
|
||||
"tools.present_files": "display:present-files",
|
||||
"tools.web_search_backends": "tools:web-access",
|
||||
"tools.web_fetch_backends": "tools:web-access",
|
||||
"tools.searxng_url": "tools:web-access",
|
||||
"observability.otlp.enabled": "account:trace-export",
|
||||
"observability.otlp.endpoint": "network:trace-export",
|
||||
"observability.otlp.header_name": "network:trace-export",
|
||||
"observability.otlp.header_env": "network:trace-export",
|
||||
"observability.otlp.service_name": "network:trace-export",
|
||||
"observability.otlp.endpoint": "account:trace-export",
|
||||
"observability.otlp.header_name": "account:trace-export",
|
||||
"observability.otlp.header_env": "account:trace-export",
|
||||
"observability.otlp.service_name": "account:trace-export",
|
||||
"tools.video_url_passthrough": "tools:web-access",
|
||||
"agent.subagent_claims": "tools:subagent-claims",
|
||||
"agent.subagent_claims": "session:subagent-claims",
|
||||
"agent.write_lease": "tools:write-lease",
|
||||
"ui.show_changed_files": "session:changed-files",
|
||||
"ui.feedback_level_notices": "session:feedback-level-notices",
|
||||
"ui.fold_turn_work": "session:fold-work",
|
||||
"ui.show_changed_files": "display:changed-files",
|
||||
"ui.feedback_level_notices": "display:feedback-level-notices",
|
||||
"ui.fold_turn_work": "display:fold-work",
|
||||
"agent.turn_narration": "session:turn-narration",
|
||||
"auto_archive.enabled": "session:auto-archive",
|
||||
"auto_archive.days": "session:auto-archive",
|
||||
"auto_archive.enabled": "storage:auto-archive",
|
||||
"auto_archive.days": "storage:auto-archive",
|
||||
"progress_watch.pause": "session:progress-watch",
|
||||
"progress_watch.rounds": "session:progress-watch",
|
||||
"progress_watch.token_multiple": "session:progress-watch",
|
||||
|
||||
@@ -32,7 +32,7 @@ const name = (el: Node): string => {
|
||||
return n?.type === "JSXIdentifier" ? (n.name as string) : "";
|
||||
};
|
||||
|
||||
/** Every settings block the tree renders, by the id it names itself with.
|
||||
/** Every settings block or row the tree renders, by the id it names itself with.
|
||||
* A hand-drawn frame counts when its direct group header carries an h3 — the
|
||||
* same structure Group renders, unlike nested labels such as Storage's. */
|
||||
function rendered(): { ids: string[]; unnamed: string[] } {
|
||||
@@ -46,6 +46,11 @@ function rendered(): { ids: string[]; unnamed: string[] } {
|
||||
else unnamed.push(`${path}:${n.loc?.start.line} <Group> with no literal id`);
|
||||
return;
|
||||
}
|
||||
if (n.type === "JSXOpeningElement" && (name(n) === "SettingRow" || name(n) === "SettingDeep")) {
|
||||
const id = literal(attr(n, "id"));
|
||||
if (id) ids.push(id);
|
||||
return;
|
||||
}
|
||||
if (n.type !== "JSXElement") return;
|
||||
const open = n.openingElement as Node;
|
||||
const declared = literal(attr(open, "data-setting"));
|
||||
@@ -150,11 +155,10 @@ describe("the settings catalogue", () => {
|
||||
expect(now).not.toContain("重启");
|
||||
});
|
||||
|
||||
// Saved and in force are two facts, and the restart answer is the one where
|
||||
// they come apart. A row that only said "after a restart" would leave the
|
||||
// reader wondering whether closing this sheet loses what they just typed.
|
||||
it("says a restart setting is already kept", () => {
|
||||
expect(renderToStaticMarkup(<Group id="storage" title="X" />)).toContain("已保存");
|
||||
// A block that differs from the page's usual says so once, as a tag beside
|
||||
// its name, instead of repeating it under every row.
|
||||
it("tags a restart setting beside its name", () => {
|
||||
expect(renderToStaticMarkup(<Group id="storage" title="X" />)).toMatch(/class="scope-tag">[^<]*重启后生效/);
|
||||
});
|
||||
|
||||
// Reporting blocks have nothing to apply, and must not claim otherwise.
|
||||
|
||||
Reference in new issue
Block a user