diff --git a/desktop/frontend-next/perf/harness.tsx b/desktop/frontend-next/perf/harness.tsx index 66a362057b..f1fa19aa40 100644 --- a/desktop/frontend-next/perf/harness.tsx +++ b/desktop/frontend-next/perf/harness.tsx @@ -273,12 +273,6 @@ class BenchPort extends MockPort { } private settingCardSent = false; - private receiptSent = false; - - async receiptSettings() { - const read = await super.receiptSettings(); - return query.has("receipt") ? { ...read, show: true } : read; - } private meterSent = false; subscribe(on: (e: WireEvent) => void) { @@ -294,14 +288,6 @@ class BenchPort extends MockPort { } }, 500); } - if (query.has("receipt") && !this.receiptSent) { - this.receiptSent = true; - const script = "cd \"C:/Users/me/work\" && cat > \"$TMPDIR/check/verify_006.py\" <<'PYEOF'\n" + "import os, re, sys, unittest\nclass T(unittest.TestCase):\n def test_a(self):\n self.assertEqual(1, 1)\n".repeat(18) + "PYEOF\npython -m unittest verify_006 2>&1 | tail -12"; - setTimeout(() => { - this.feed({ kind: "turn_started", msgIndex: 9 } as unknown as WireEvent); - this.feed({ kind: "turn_done", receipt: { saysSomething: true, severity: "warn", gaps: [{ kind: "failed_verification", detail: script }, { kind: "inconclusive_verification", detail: "go test ./internal/... 2>&1 | tail -5" }] } } as unknown as WireEvent); - }, 500); - } const card = query.get("settingcard"); if (card && !this.settingCardSent) { this.settingCardSent = true; @@ -653,7 +639,7 @@ function settingCardFrame(kind: string): WireEvent { return { kind: "notice", level: "info", code: "setting_open", text: "助手需要改动 permissions.mode,只有你本人能改。", detail: "permissions.mode" } as WireEvent; } const elevated = kind === "elevated"; - const id = elevated ? "peers.dispatch" : "ui.show_receipt"; + const id = elevated ? "peers.dispatch" : "ui.show_changed_files"; return { kind: "approval_request", approval: { diff --git a/desktop/frontend-next/src/actions.ts b/desktop/frontend-next/src/actions.ts index 838998e7c1..5d2edcd474 100644 --- a/desktop/frontend-next/src/actions.ts +++ b/desktop/frontend-next/src/actions.ts @@ -153,8 +153,6 @@ export const ACTIONS: UIAction[] = [ { id: "chrome.fold-work", kind: "kernel-mutation", target: "none", proof: "interaction" }, { id: "chrome.turn-narration", kind: "kernel-mutation", target: "none", proof: "interaction" }, { id: "changes.open", kind: "navigation", target: "none", proof: "interaction" }, - { id: "chrome.receipt", kind: "view", target: "none", proof: "interaction" }, - { id: "chrome.receipt-line", kind: "view", target: "none", proof: "interaction" }, { id: "chrome.keep-awake", kind: "view", target: "none", proof: "interaction" }, { id: "composer.send-shortcut", kind: "view", target: "none", proof: "interaction" }, { id: "composer.running-message-intent", kind: "view", target: "none", proof: "interaction" }, diff --git a/desktop/frontend-next/src/design/turn.css b/desktop/frontend-next/src/design/turn.css index 909ececad3..e54c4a6253 100644 --- a/desktop/frontend-next/src/design/turn.css +++ b/desktop/frontend-next/src/design/turn.css @@ -162,3 +162,21 @@ .flow .call[data-k="host"][data-light] .comp details > summary { color: var(--text); font: var(--w-med) var(--fs-meta)/1.5 var(--ui); cursor: pointer; } .flow .call[data-k="host"][data-light] > .c > .hl { border: 0; } .flow .call[data-k="host"][data-light] > .c > .out { margin: 0; padding: 0; } +.flow .note { margin: 10px 0; padding: 2px 0 2px 14px; border-left: 2px solid var(--hair); } +.flow .note[data-lvl="warn"] { border-left-color: var(--warn); } +.flow .note[data-lvl="err"] { border-left-color: var(--err); } +.note-hd { display: flex; align-items: center; gap: 8px; min-height: 24px; } +.note-hd .sym { color: var(--faint); } +.note[data-lvl="warn"] .note-hd .sym { color: var(--warn-ink); } +.note[data-lvl="err"] .note-hd .sym { color: var(--err-ink); } +.note-nm { flex: none; color: var(--text); font: var(--w-semi) var(--fs-ui)/1.5 var(--ui); } +.note[data-lead] .note-nm { color: var(--muted); font-weight: var(--w-reg); } +.note-strong { min-width: 0; overflow: hidden; color: var(--text); font: var(--w-semi) var(--fs-ui)/1.5 var(--ui); text-overflow: ellipsis; white-space: nowrap; } +.note-meta { flex: 0 1 auto; color: var(--faint); font: var(--w-reg) var(--fs-meta)/1.5 var(--ui); white-space: nowrap; } +.note-act { margin-left: auto; flex: none; padding: 0; border: 0; background: none; color: var(--text); font: var(--w-semi) var(--fs-meta)/1.5 var(--ui); cursor: pointer; } +.note-act:hover { text-decoration: underline; } +.note-act:disabled { color: var(--faint); cursor: default; text-decoration: none; } +.note-subject { margin: 2px 0 0 22px; overflow: hidden; color: var(--muted); font: var(--w-reg) var(--fs-meta)/1.5 var(--mono); text-overflow: ellipsis; white-space: nowrap; } +.note-text { margin: 2px 0 0 22px; color: var(--muted); font: var(--w-reg) var(--fs-meta)/1.6 var(--ui); overflow-wrap: anywhere; } +.note[data-gone] { opacity: .6; } +.note[data-gone] .note-strong { text-decoration: line-through; } diff --git a/desktop/frontend-next/src/i18n/kernel.ts b/desktop/frontend-next/src/i18n/kernel.ts index e6b7878975..8ff07940cc 100644 --- a/desktop/frontend-next/src/i18n/kernel.ts +++ b/desktop/frontend-next/src/i18n/kernel.ts @@ -616,7 +616,6 @@ const SAID: Record = { "ask_timeout.out_of_range": "kernel.that_wait_time_is_out", "session_ref.save_failed": "kernel.the_size_limit_for_referenced", "session_ref.out_of_range": "kernel.the_size_limit_is_outside", - "receipt.save_failed": "kernel.the_receipt_card_setting_was", "progress_watch.save_failed": "kernel.the_progress_settings_were_not", "auto_archive.save_failed": "kernel.the_auto_archive_settings_were", "auto_archive.out_of_range": "kernel.the_number_of_days_is", diff --git a/desktop/frontend-next/src/i18n/msg/chat.ts b/desktop/frontend-next/src/i18n/msg/chat.ts index 0243627da6..7e67a9e8a6 100644 --- a/desktop/frontend-next/src/i18n/msg/chat.ts +++ b/desktop/frontend-next/src/i18n/msg/chat.ts @@ -1,7 +1,6 @@ import type { Messages } from "./types"; export const CHAT: Messages = { - "chat.a_claude_code_mod_was": { zh: "有 Claude Code mod 在运行;它写了什么不在主机观测范围内", en: "a Claude Code mod was active; what it wrote is outside what the host observes" }, "chat.a_file_outside_this_subtasks": { zh: "这个子任务声明之外的文件", en: "A file outside this subtask's declared paths" }, "chat.a_run_of_tool_calls": { zh: "一轮中连续的工具调用,收起后只剩一行摘要", en: "A run of tool calls within a turn; folded, it is a one-line summary" }, "chat.a_subagent_running_under_the": { zh: "按技能 {name} 的设定运行的子代理", en: "A subagent running under the {name} skill's settings" }, @@ -30,11 +29,7 @@ export const CHAT: Messages = { "chat.always_allow_is_saved_as": { zh: "“始终允许”写入用户配置的权限规则,可在设置 → 工具与权限里撤销。", en: "“Always allow” is saved as a permission rule in your user config and can be undone in Settings → Tools and permissions." }, "chat.always_allow_records_this_sign": { zh: "“始终允许”把这个登录域名记在该服务器自己的登录状态旁,之后同一服务器登录它不再询问。", en: "“Always allow” records this sign-in domain beside that server’s own sign-in state, so the same server is not asked about it again." }, "chat.always_allow_this_kind_stop": { zh: "始终允许此类操作,不再询问", en: "Always allow this kind, stop asking" }, - "chat.an_acceptance_criterion_has_no": { zh: "验收项没有证据", en: "an acceptance criterion has no evidence" }, - "chat.an_expected_check_never_passed": { zh: "预期的检查从未通过", en: "an expected check never passed" }, - "chat.and_n_more": { zh: "另有 {n} 项", en: "and {n} more" }, "chat.and_n_more_rows": { zh: "另有 {n} 行", en: "and {n} more rows" }, - "chat.another_session_wrote_under_this": { zh: "你在等待期间,另一个会话改动了本会话占用的路径;之前读到的内容可能已过时", en: "another session wrote under this session's paths while it waited on you; earlier reads there may be stale" }, "chat.answered_on_device_n": { zh: "由 设备 {n} 回答", en: "Answered on device {n}" }, "chat.answered_on_device_n_said": { zh: "由 设备 {n} 回答:{said}", en: "Answered on device {n}: {said}" }, "chat.answered_on_the_computer": { zh: "在电脑上回答", en: "Answered on the computer" }, @@ -71,8 +66,6 @@ export const CHAT: Messages = { "chat.capacity_guard": { zh: "容量保护", en: "Capacity guard" }, "chat.card_view": { zh: "卡片视图", en: "Card view" }, "chat.changed_n_files": { zh: "改了{n}个文件", en: "changed {n} files" }, - "chat.changed_never_looked_at_again": { zh: "改动后未再检查", en: "changed, never looked at again" }, - "chat.changed_no_check_the_host": { zh: "有改动;没有主机能读作证据的检查", en: "changed; no check the host could read as proof" }, "chat.changes_made_by_bash_commands": { zh: "bash 命令所作的修改没有快照", en: "Changes made by bash commands have no snapshot" }, "chat.changes_only": { zh: "只看改动", en: "Changes only" }, "chat.chart": { zh: "图表", en: "Chart" }, @@ -82,10 +75,8 @@ export const CHAT: Messages = { "chat.check_the_full_address_this": { zh: "请核对完整地址:此链接使用 HTTP 或国际化域名。", en: "Check the full address: this link uses HTTP or an internationalized domain." }, "chat.choose_enter_adds_it_to": { zh: "↑↓ 选择 · 回车添加到这条消息 · Esc 关闭", en: "↑↓ choose · Enter adds it to this message · Esc closes" }, "chat.choose_places_and_arrange_the": { zh: "选择地点并调整访问顺序,地图与行程同步显示", en: "Choose places and arrange the visit order with the map alongside." }, - "chat.claimed_but_the_ledger_does": { zh: "声称过但账本不支持", en: "claimed, but the ledger does not back it" }, "chat.classifying": { zh: "分类", en: "Classifying" }, "chat.click_to_jump_right_click": { zh: "点击跳过去 · 右键加星标", en: "Click to jump · right-click to star" }, - "chat.click_to_see_the_delivery": { zh: "点击查看交付验收:改了什么、哪些没有验证", en: "Click to see the delivery receipt: what changed and what was not verified" }, "chat.clipped": { zh: "截断", en: "Clipped" }, "chat.close_find": { zh: "关闭查找", en: "Close find" }, "chat.close_image": { zh: "关闭图片", en: "Close image" }, @@ -94,7 +85,6 @@ export const CHAT: Messages = { "chat.code_and_conversation": { zh: "代码和对话", en: "Code and conversation" }, "chat.code_diagnostics": { zh: "代码诊断", en: "Code diagnostics" }, "chat.code_only": { zh: "只还原代码", en: "Code only" }, - "chat.collapse_the_delivery_receipt": { zh: "收起交付验收", en: "Collapse the delivery receipt" }, "chat.collapse_workspace": { zh: "收起工作区", en: "Collapse workspace" }, "chat.command_output": { zh: "命令输出", en: "Command output" }, "chat.command_output_and_file_contents": { zh: "超出一屏的命令输出与文件内容", en: "Command output and file contents longer than a screen" }, @@ -129,7 +119,6 @@ export const CHAT: Messages = { "chat.data_dashboard": { zh: "数据仪表盘", en: "Data dashboard" }, "chat.data_details": { zh: "数据明细", en: "Data details" }, "chat.declare_once_the_reason_is": { zh: "在查清原因后声明任务有一部分按要求做不了,或需要你来决定。", en: "Declare, once the reason is established, that part of the task cannot be done as asked or needs your decision." }, - "chat.declared_unverified_by_the_turn": { zh: "自己申报未验证", en: "declared unverified by the turn itself" }, "chat.decline": { zh: "拒绝提供", en: "Decline" }, "chat.declined": { zh: "已拒绝提供", en: "Declined" }, "chat.delegate_task": { zh: "委派任务", en: "Delegate task" }, @@ -139,7 +128,6 @@ export const CHAT: Messages = { "chat.delete_content": { zh: "删除内容", en: "Delete content" }, "chat.delete_symbol": { zh: "删除符号", en: "Delete symbol" }, "chat.delivered_files": { zh: "交付文件", en: "Delivered files" }, - "chat.delivery_receipt": { zh: "交付验收", en: "delivery receipt" }, "chat.denied": { zh: "已拒绝。", en: "Denied." }, "chat.deny_rules_and_sandbox_boundaries": { zh: "拒绝规则与沙盒边界仍然生效。仅在完全信任当前工作区时使用。", en: "Deny rules and sandbox boundaries still apply. Use only when you fully trust this workspace." }, "chat.describe_the_task_reasonix_takes": { zh: "描述任务,其余交给 Reasonix", en: "Describe the task; Reasonix takes it from there" }, @@ -194,7 +182,6 @@ export const CHAT: Messages = { "chat.handled_in_another_window_refer": { zh: "已在其他窗口处理,请以最新运行状态为准。", en: "Handled in another window. Refer to the latest run state." }, "chat.hide_data": { zh: "收起数据", en: "Hide data" }, "chat.history_could_not_be_loaded": { zh: "历史加载失败 · 重试", en: "History could not be loaded · Retry" }, - "chat.host": { zh: "主机", en: "Host" }, "chat.hover_or_focus_the_chart": { zh: "悬停或聚焦图表以查看数值", en: "Hover or focus the chart to see values" }, "chat.image_source": { zh: "图片来源", en: "Image source" }, "chat.image_unavailable": { zh: "图片不可用", en: "Image unavailable" }, @@ -206,7 +193,6 @@ export const CHAT: Messages = { "chat.invoke_a_skill_from_the": { zh: "调用系统提示里技能索引中的一份技能手册。", en: "Invoke a skill from the skills index in the system prompt." }, "chat.invoke_a_skill_read_only": { zh: "以只读方式调用一个技能;子代理类技能在只读的隔离会话里运行。", en: "Invoke a skill read-only; sub-agent skills run in an isolated read-only session." }, "chat.it_can_read_code_research": { zh: "可读取代码、查找资料、运行工具并修改文件。完整执行过程可在「运行分析」中查看。", en: "It can read code, research, run tools, and edit files. The full run stays visible in the Trajectory." }, - "chat.it_ran_but_the_exit": { zh: "已运行,但退出码来自命令中的其他片段", en: "it ran, but the exit code came from another stage of the command" }, "chat.its_own_context_stays_out": { zh: "独立上下文 · 不进主轨迹", en: "Its own context · stays out of the main trajectory" }, "chat.jump_to_latest": { zh: "跳到最新", en: "Jump to latest" }, "chat.jump_to_latest_shift_click": { zh: "跳到最新(Shift+点击:下一个展开的块)", en: "Jump to latest (Shift+click: next expanded block)" }, @@ -273,7 +259,6 @@ export const CHAT: Messages = { "chat.nobody_answered_in_time_this": { zh: "等待超时,这个问题已被跳过", en: "Nobody answered in time; this question was skipped" }, "chat.note_optional": { zh: "补充说明(可选)", en: "Note (optional)" }, "chat.notes_count": { zh: "文字 {count}", en: "Notes: {count}" }, - "chat.nothing_left_unverified": { zh: "没有未经验证的部分", en: "nothing left unverified" }, "chat.nothing_to_draw": { zh: "没有可画的数据", en: "Nothing to draw" }, "chat.notice": { zh: "提示", en: "Notice" }, "chat.office_preview_is_off": { zh: "没有开启 Office 文档预览", en: "Office document preview is off" }, @@ -463,7 +448,6 @@ export const CHAT: Messages = { "chat.the_same_operation_will_ask": { zh: "下次同样的操作仍会请求确认。", en: "The same operation will ask again next time." }, "chat.the_table_is_invalid_or": { zh: "表格无法解析或超过 10 万个单元格,请切换源码查看", en: "The table is invalid or exceeds 100,000 cells. Switch to source to inspect it." }, "chat.the_turn_in_progress_holds": { zh: "正在进行的这一轮占 {n},折叠动不了它。", en: "The turn in progress holds {n} of it, and a fold cannot take it." }, - "chat.the_verification_predates_the_last": { zh: "验证早于最后一次改动", en: "the verification predates the last change" }, "chat.thinking": { zh: "思考中…", en: "Thinking…" }, "chat.thinking_2": { zh: "思考过程", en: "Thinking" }, "chat.thinking_and_notes": { zh: "思考与中间文字", en: "Thinking and notes" }, @@ -493,8 +477,6 @@ export const CHAT: Messages = { "chat.this_surface_has_expired_wait": { zh: "界面已失效,请等待新的请求", en: "This surface has expired. Wait for a new request." }, "chat.this_turn": { zh: "本轮执行", en: "This turn" }, "chat.this_turn_cannot_be_regenerated": { zh: "这一轮无法重新生成", en: "This turn cannot be regenerated" }, - "chat.this_turn_declared_risks_or": { zh: "这一轮自己说明了风险或未验证项", en: "This turn declared risks or unverified items" }, - "chat.this_turn_said_so_itself": { zh: "这一轮自己说明的", en: "this turn said so itself" }, "chat.this_workbook_has_no_visible": { zh: "这个工作簿没有可见的工作表", en: "This workbook has no visible sheets." }, "chat.thought_chars": { zh: "思考 {chars}", en: "Thought · {chars}" }, "chat.thought_chars_secs_s": { zh: "思考 {secs} 秒 · {chars}", en: "Thought {secs}s · {chars}" }, @@ -508,10 +490,8 @@ export const CHAT: Messages = { "chat.transcript_folding": { zh: "会话折叠", en: "Transcript folding" }, "chat.triggered_by_hand": { zh: "手动触发", en: "Triggered by hand" }, "chat.unanswered": { zh: "未答", en: "Unanswered" }, - "chat.unverified_changes": { zh: "有未验证的改动", en: "Unverified changes" }, "chat.update_goal": { zh: "更新目标", en: "Update goal" }, "chat.update_plan": { zh: "更新计划", en: "Update plan" }, - "chat.verification_failed": { zh: "验证失败", en: "verification failed" }, "chat.video_name_detail_sent_to": { zh: "视频 {name}({detail})已原生发送给 {model}", en: "Video {name} ({detail}) sent to {model} as a video file" }, "chat.view_data": { zh: "查看数据", en: "View data" }, "chat.view_image_alt": { zh: "查看大图:{alt}", en: "View image: {alt}" }, @@ -528,7 +508,6 @@ export const CHAT: Messages = { "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." }, - "chat.written_outside_the_folder_this": { zh: "写在了主机观测范围之外;这里没有任何证据能说明它留下了什么", en: "written outside the folder this host watches; nothing here can say what it left" }, "chat.written_to_the_config_later": { zh: "已写入配置,后续会话中此类操作也始终允许,不再询问。", en: "Written to the config; later sessions will also always allow this kind without asking." }, "chat.you": { zh: "你", en: "You" }, "chat.your_message_n": { zh: "你的第{n}条", en: "Your message {n}" }, diff --git a/desktop/frontend-next/src/i18n/msg/kernel.ts b/desktop/frontend-next/src/i18n/msg/kernel.ts index e44cede375..018aa5b3ca 100644 --- a/desktop/frontend-next/src/i18n/msg/kernel.ts +++ b/desktop/frontend-next/src/i18n/msg/kernel.ts @@ -557,7 +557,6 @@ export const KERNEL: Messages = { "kernel.the_queue_record_was_corrupt": { zh: "队列记录损坏,这一条是从残留文件里找回的", en: "The queue record was corrupt; this item was salvaged from leftover files" }, "kernel.the_reasonix_installed_on_host": { zh: "安装到 {host} 上的 reasonix 无法运行。该目录可能挂载了 noexec,也可能传输中断", en: "The reasonix installed on {host} will not run. That directory may be mounted noexec, or the transfer was cut short" }, "kernel.the_reasonix_on_host_is": { zh: "{host} 上的 reasonix 版本过旧,无法打开面板 —— 请先将该机器上的 reasonix 升级至与本机同一代", en: "The reasonix on {host} is too old to open a pane — upgrade that machine's reasonix to the same line as this one first" }, - "kernel.the_receipt_card_setting_was": { zh: "交付验收卡片的设置未能保存:{detail}", en: "The receipt card setting was not saved: {detail}" }, "kernel.the_recursive_delete_extent_is": { zh: "无法确定递归删除范围;请使用字面命令名、路径和解释器内容", en: "The recursive delete extent is unknown; use a literal command name, path and interpreter payload" }, "kernel.the_recursive_delete_target_is": { zh: "递归删除目标受保护或超出授权范围;请使用工作区或授权目录内的字面路径", en: "The recursive delete target is protected or outside the granted roots; use a literal path inside the workspace or a granted directory" }, "kernel.the_refine_request_was_malformed": { zh: "优化请求格式不正确", en: "The refine request was malformed" }, diff --git a/desktop/frontend-next/src/i18n/msg/settings.ts b/desktop/frontend-next/src/i18n/msg/settings.ts index 7e9d0a5eb2..524f5dc1ed 100644 --- a/desktop/frontend-next/src/i18n/msg/settings.ts +++ b/desktop/frontend-next/src/i18n/msg/settings.ts @@ -18,7 +18,6 @@ export const SETTINGS: Messages = { "settings.applies_to_new_sessions": { zh: "新开的会话生效", en: "Applies to new sessions" }, "settings.apply_shortcut": { zh: "应用快捷键", en: "Apply shortcut" }, "settings.approval": { zh: "批准", en: "Approval" }, - "settings.at_the_end_of_each": { zh: "每一轮结束后,主机会在会话末尾列出这一轮改了什么、验证了什么、哪些没有验证。记录始终保存,这里只决定是否显示。", en: "At the end of each turn the host lists what the turn changed, what it verified and what it did not. The record is always kept; this only decides whether it is shown." }, "settings.automation": { zh: "自动化", en: "Automation" }, "settings.brief": { zh: "简短", en: "Brief" }, "settings.cannot_reach_the_identity_service": { zh: "无法连接身份服务", en: "Cannot reach the identity service" }, @@ -30,7 +29,6 @@ export const SETTINGS: Messages = { "settings.closing_the_window_does_not": { zh: "关闭窗口不会中断会话和后台任务;从托盘菜单退出才会完全关闭程序", en: "Closing the window does not stop sessions or background tasks; use Quit in the tray menu to exit completely" }, "settings.collaboration": { zh: "协作", en: "Collaboration" }, "settings.common": { zh: "常用", en: "Common" }, - "settings.completion_standard": { zh: "完成标准", en: "Completion standard" }, "settings.confirm_sign_out": { zh: "确认退出", en: "Confirm sign-out" }, "settings.context_maintenance": { zh: "上下文维护", en: "Context maintenance" }, "settings.controls_and_performance": { zh: "操作与性能", en: "Controls and performance" }, @@ -39,7 +37,6 @@ export const SETTINGS: Messages = { "settings.could_not_save_the_shortcut": { zh: "快捷键未能保存;原设置保持不变。", en: "Could not save the shortcut; the previous setting is unchanged." }, "settings.data": { zh: "数据", en: "Data" }, "settings.default_0_nothing_is_folded": { zh: "默认 0:不自动收起,打开的会话都保留。设成一个数字后,打开的会话超过它时,会收起最久没动的空闲会话;对话仍在列表里,随时可以重新打开。保留得越多,占用的内存越多。", en: "Default 0: nothing is folded away and every open session stays. Set a number and, once more sessions than that are open, the idle one untouched longest is folded away; its conversation stays in the list and can be opened again. The more you keep, the more memory it uses." }, - "settings.delivery_receipt_card": { zh: "交付验收卡片", en: "Delivery receipt card" }, "settings.detailed": { zh: "详细", en: "Detailed" }, "settings.details": { zh: "细项", en: "Details" }, "settings.door_does_not_offer_model": { zh: "{door} 不提供 {model},请在下方选择该来源支持的模型。", en: "{door} does not offer {model}. Choose one it does provide below." }, @@ -49,7 +46,6 @@ export const SETTINGS: Messages = { "settings.english": { zh: "英文", en: "English" }, "settings.enlarge_the_settings_window": { zh: "放大设置窗口", en: "Enlarge the settings window" }, "settings.environment_variables": { zh: "环境变量", en: "Environment variables" }, - "settings.everyday_questions_stay_lightweight_file": { zh: "日常问答保持轻量;涉及文件修改、代码执行或明确交付物时自动增加验证与复核。", en: "Everyday questions stay lightweight; file changes, code execution, and explicit deliverables automatically receive more verification and review." }, "settings.external_tools": { zh: "外部工具", en: "External tools" }, "settings.file_changes": { zh: "文件改动", en: "File changes" }, "settings.find_in_the_conversation": { zh: "在对话里查找", en: "Find in the conversation" }, @@ -152,7 +148,6 @@ export const SETTINGS: Messages = { "settings.show_a_status_icon_in": { zh: "在托盘显示图标", en: "Show a status icon in the tray" }, "settings.show_file_changes_above_the": { zh: "在输入框上方显示文件改动", en: "Show file changes above the input box" }, "settings.show_name": { zh: "显示:{name}", en: "Show: {name}" }, - "settings.show_the_delivery_receipt_card": { zh: "显示交付验收卡片", en: "Show the delivery receipt card" }, "settings.shrink_the_settings_window": { zh: "缩小设置窗口", en: "Shrink the settings window" }, "settings.sign_in_status_could_not": { zh: "无法读取登录状态", en: "Sign-in status could not be read" }, "settings.sign_out": { zh: "退出登录", en: "Sign out" }, diff --git a/desktop/frontend-next/src/port/boundary.ts b/desktop/frontend-next/src/port/boundary.ts index a3db458893..187e893d3d 100644 --- a/desktop/frontend-next/src/port/boundary.ts +++ b/desktop/frontend-next/src/port/boundary.ts @@ -363,13 +363,6 @@ export interface SessionRefSettings { path: string; } -// Whether the end-of-turn delivery receipt is drawn as a card; the user file -// holds it, so the terminal reads the same answer. -export interface ReceiptSettings { - show: boolean; - path: string; -} - export interface ProgressWatchSettings { pause: boolean; rounds: number; diff --git a/desktop/frontend-next/src/port/mock_boundary.ts b/desktop/frontend-next/src/port/mock_boundary.ts index 090220a114..24f326bd91 100644 --- a/desktop/frontend-next/src/port/mock_boundary.ts +++ b/desktop/frontend-next/src/port/mock_boundary.ts @@ -1,5 +1,5 @@ import type { Adjudications, BrowserToolsSettings, SubagentClaimsSettings, WriteLeaseSettings, PlanApprovalSettings, DefaultModeSettings, ToolSettings, PeerReceipt, PeersState, PeersSettings, PeersSettingsEdit, PeerRemember, ConfigProblem, ConfigRepair, PermissionLists, PermissionRules, SandboxSettings, UpdateMode } from "./port"; -import type { AskTimeoutSettings, ModStatus, ModsSettings, ModsSettingsEdit, SessionRefSettings, AutoArchiveSettings, ChangedFilesSettings, FeedbackLevelNotices, FoldWorkSettings, TurnNarrationSettings, ObservabilityPatch, ObservabilitySettings, WebAccessPatch, WebAccessSettings, SearxngProbe, DisplayCurrencyMode, DisplayCurrencySettings, ProgressWatchSettings, WorkspaceRoots, ReceiptSettings } from "./boundary"; +import type { AskTimeoutSettings, ModStatus, ModsSettings, ModsSettingsEdit, SessionRefSettings, AutoArchiveSettings, ChangedFilesSettings, FeedbackLevelNotices, FoldWorkSettings, TurnNarrationSettings, ObservabilityPatch, ObservabilitySettings, WebAccessPatch, WebAccessSettings, SearxngProbe, DisplayCurrencyMode, DisplayCurrencySettings, ProgressWatchSettings, WorkspaceRoots } from "./boundary"; import { mockMods } from "./mock_mods"; import type { ScheduleBoard, ScheduleDraft, SchedulePreview, ScheduleResultView, ScheduleView } from "./schedule"; import { HttpError } from "./http_error"; @@ -496,17 +496,6 @@ export class MockBoundary extends MockShell { return { ...this.sessionRef }; } - private receipt: ReceiptSettings = { show: false, path: "/Users/you/.reasonix/config.toml" }; - - async receiptSettings(): Promise { - return { ...this.receipt }; - } - - async saveReceiptSettings(show: boolean): Promise { - this.receipt = { ...this.receipt, show }; - return { ...this.receipt }; - } - async progressWatch(): Promise { return { ...this.watch }; } diff --git a/desktop/frontend-next/src/port/port.ts b/desktop/frontend-next/src/port/port.ts index bcbf103a96..66d8b08de0 100644 --- a/desktop/frontend-next/src/port/port.ts +++ b/desktop/frontend-next/src/port/port.ts @@ -46,7 +46,7 @@ import type { ExecutionGraphRead, TrajectoryRead, WireEvent } from "./wire"; import type { PluginExport, PluginInstallRequest, PluginPackage, PluginPlan } from "./plugin"; import type { MarketDetail, MarketList, MarketOwnRequest, MarketPackage, MarketPlan, MarketPublished, MarketQuery, MarketRequest, MarketSubmission, MarketVote } from "./market"; import type { Appearance, SceneFolder, ThemeImport, ThemePack } from "./look"; -import type { AskTimeoutSettings, ModStatus, ModsSettings, ModsSettingsEdit, SessionRefSettings, AutoArchiveSettings, ChangedFilesSettings, FeedbackLevelNotices, FoldWorkSettings, TurnNarrationSettings, ObservabilityPatch, ObservabilitySettings, WebAccessPatch, WebAccessSettings, SearxngProbe, BrowserToolsSettings, SubagentClaimsSettings, WriteLeaseSettings, PlanApprovalSettings, DefaultModeSettings, ToolSettings, PeerReceipt, PeersState, PeersSettings, PeersSettingsEdit, PeerRemember, ConfigProblem, ConfigRepair, DisplayCurrencyMode, DisplayCurrencySettings, PermissionLists, PermissionRules, ProgressWatchSettings, SandboxSettings, WorkspaceRoots, ReceiptSettings } from "./boundary"; +import type { AskTimeoutSettings, ModStatus, ModsSettings, ModsSettingsEdit, SessionRefSettings, AutoArchiveSettings, ChangedFilesSettings, FeedbackLevelNotices, FoldWorkSettings, TurnNarrationSettings, ObservabilityPatch, ObservabilitySettings, WebAccessPatch, WebAccessSettings, SearxngProbe, BrowserToolsSettings, SubagentClaimsSettings, WriteLeaseSettings, PlanApprovalSettings, DefaultModeSettings, ToolSettings, PeerReceipt, PeersState, PeersSettings, PeersSettingsEdit, PeerRemember, ConfigProblem, ConfigRepair, DisplayCurrencyMode, DisplayCurrencySettings, PermissionLists, PermissionRules, ProgressWatchSettings, SandboxSettings, WorkspaceRoots } from "./boundary"; import type { ScheduleBoard, ScheduleDraft, SchedulePreview, ScheduleResultView, ScheduleView } from "./schedule"; import type { Protocol, ProviderHealth, ProviderCheck, ProviderCheckDraft, ProviderDraft, ProviderEdit, ProviderEntry, ProviderModelCheck, ProviderModelCheckRequest, ProviderProbe, ProviderSetup, ProviderRemoval, ProviderPreset, ProviderPresetInstall } from "./provider"; export type { ModelEffort, ModelLimit, ModelRate, Protocol, ProviderHealth, ProviderCheck, ProviderCheckDraft, ProviderDraft, ProviderEdit, ProviderEntry, ProviderModelCheck, ProviderModelCheckRequest, ProviderProbe, ProviderSetup, ProviderRemoval, ProviderPreset, ProviderPresetInstall } from "./provider"; @@ -361,8 +361,6 @@ export interface AgentPort extends WorkspacePreviewPort, SessionTailPort, Upload // The persona is part of the cache-stable prefix, composed once at boot: // saving is written now and read by the next session, not this one's runtime. savePersonaSettings(s: PersonaDraft): Promise; - receiptSettings(): Promise; - saveReceiptSettings(show: boolean): Promise; // null in a browser tab: there is no window to keep running. trayPrefs(): Promise; setTrayPrefs(icon: boolean, closeToTray: boolean): Promise; diff --git a/desktop/frontend-next/src/port/sse_boundary.ts b/desktop/frontend-next/src/port/sse_boundary.ts index f19a707bbf..97740b96f7 100644 --- a/desktop/frontend-next/src/port/sse_boundary.ts +++ b/desktop/frontend-next/src/port/sse_boundary.ts @@ -4,7 +4,7 @@ import type { AgentComposition, AgentPresetVersion, AgentPresetCatalog, Composit import type { ScheduleBoard, ScheduleDraft, SchedulePreview, ScheduleResultView, ScheduleView } from "./schedule"; import type { PersonaDraft, PersonaSettings } from "./boundary"; import type { Adjudications, BrowserToolsSettings, SubagentClaimsSettings, WriteLeaseSettings, PlanApprovalSettings, DefaultModeSettings, ToolSettings, PeerReceipt, PeersState, PeersSettings, PeersSettingsEdit, PeerRemember, ConfigProblem, ConfigRepair, PermissionLists, PermissionRules, SandboxSettings } from "./port"; -import type { AskTimeoutSettings, ModStatus, ModsSettings, ModsSettingsEdit, SessionRefSettings, AutoArchiveSettings, ChangedFilesSettings, FeedbackLevelNotices, FoldWorkSettings, TurnNarrationSettings, ObservabilityPatch, ObservabilitySettings, WebAccessPatch, WebAccessSettings, SearxngProbe, DisplayCurrencyMode, DisplayCurrencySettings, ProgressWatchSettings, WorkspaceRoots, ReceiptSettings } from "./boundary"; +import type { AskTimeoutSettings, ModStatus, ModsSettings, ModsSettingsEdit, SessionRefSettings, AutoArchiveSettings, ChangedFilesSettings, FeedbackLevelNotices, FoldWorkSettings, TurnNarrationSettings, ObservabilityPatch, ObservabilitySettings, WebAccessPatch, WebAccessSettings, SearxngProbe, DisplayCurrencyMode, DisplayCurrencySettings, ProgressWatchSettings, WorkspaceRoots } from "./boundary"; // Where the agent may reach: the permission rules a call is matched against and // the sandbox the shell runs in. @@ -206,12 +206,6 @@ export class SseBoundary extends SseShell { saveSessionRefSettings(tokens: number) { return this.post0("/session-ref-settings", { tokens }); } - receiptSettings() { - return this.get("/receipt-settings"); - } - saveReceiptSettings(show: boolean) { - return this.post0("/receipt-settings", { show }); - } configProblem() { return this.get("/config/problem"); } diff --git a/desktop/frontend-next/src/port/wire.ts b/desktop/frontend-next/src/port/wire.ts index 47ecac59be..bfe132f666 100644 --- a/desktop/frontend-next/src/port/wire.ts +++ b/desktop/frontend-next/src/port/wire.ts @@ -568,45 +568,6 @@ export interface MemoryCitation { kind?: string; } -// The user-facing completion record on turn_done: what the host could verify -// about the turn's work and, the part no prose reliably carries, what it could -// not. The quality summary beside it is a machine record and belongs in the -// trajectory; this is the one written for a person. -export interface Receipt { - verdict: string; - changes?: ReceiptChange[]; - verifications?: ReceiptVerification[]; - gaps?: ReceiptGap[]; - // Declarations the turn volunteered, kept apart from the gaps the host found. - // Folding them together reads a caveat somebody offered as a failure. - risks?: string[]; - unverified?: string[]; - // The kernel's own answer to "is this worth showing at all", carried rather - // than recomputed here so the windows and the terminal never drift apart. - saysSomething?: boolean; - // The kernel's grading of what was left unproven; absent when nothing was. - severity?: "notice" | "warn"; -} - -export interface ReceiptChange { - path: string; - reviewed: boolean; -} - -// stale: it ran before the newest change, so it proves nothing about the tree -// as it stands. inconclusive: the shell reported a later stage's status. -export interface ReceiptVerification { - command: string; - passed: boolean; - stale?: boolean; - inconclusive?: boolean; -} - -export interface ReceiptGap { - kind: string; - detail?: string; -} - // The run's execution graph, published as facts rather than drawn from id // prefixes. Mirrors internal/contract/agentgraph: a node named again updates the one // already there, and an edge kind is what tells "waited for this" apart from @@ -731,7 +692,6 @@ export interface WireEvent { streamAttempt?: StreamAttempt; completion?: CompletionSummary; graph?: GraphDelta; - receipt?: Receipt; decisionReceipt?: DecisionReceipt; err?: string; // turn_done: the user asked to stop. A cancelled turn and a dropped diff --git a/desktop/frontend-next/src/state/find.ts b/desktop/frontend-next/src/state/find.ts index 72e20be49e..8c20439160 100644 --- a/desktop/frontend-next/src/state/find.ts +++ b/desktop/frontend-next/src/state/find.ts @@ -42,14 +42,6 @@ export function saidBy(it: Item): string[] { return [it.m.title, it.m.description, it.m.body]; case "worked": return []; - case "receipt": - return [ - ...(it.r.changes ?? []).map((c) => c.path), - ...(it.r.verifications ?? []).map((v) => v.command), - ...(it.r.gaps ?? []).map((g) => `${g.kind} ${g.detail ?? ""}`), - ...(it.r.risks ?? []), - ...(it.r.unverified ?? []), - ]; case "extension": return [it.ext.pluginId, it.ext.card?.title ?? "", it.ext.card?.text ?? "", it.ext.card?.markdown ?? "", it.ext.notification?.title ?? "", it.ext.notification?.body ?? "", it.ext.status?.label ?? ""]; @@ -162,13 +154,12 @@ export function afterMatch(finder: Finder, hay: string, m: Match): Match | null * then speaks for what was scanned. */ const BUDGET_MS = 50; -export function search(items: Item[], query: string, receipts = false, mode: FindMode = {}): Hit[] { +export function search(items: Item[], query: string, mode: FindMode = {}): Hit[] { const finder = finderFor(query, mode); if (!finder) return []; const out: Hit[] = []; const deadline = Date.now() + BUDGET_MS; for (let row = 0; row < items.length; row++) { - if (items[row].t === "receipt" && !receipts) continue; if (Date.now() > deadline) break; const hay = finder.verbatim ? exactHaystack(items[row]) : haystack(items[row]); for (let m = finder.match(hay, 0); m; m = afterMatch(finder, hay, m)) { diff --git a/desktop/frontend-next/src/state/foldpref.ts b/desktop/frontend-next/src/state/foldpref.ts index 77008e3d51..ef2d493f22 100644 --- a/desktop/frontend-next/src/state/foldpref.ts +++ b/desktop/frontend-next/src/state/foldpref.ts @@ -1,7 +1,6 @@ -import { createContext, useContext, useRef, useSyncExternalStore } from "react"; +import { createContext, useRef, useSyncExternalStore } from "react"; import type { Item } from "./session_types"; import { foldModes, onFoldModesChange, showsThinkingPreview, type Fold, type FoldMode, type FoldModes } from "./prefs"; -import { receiptPrefOf, type ReceiptPref, type ReceiptState } from "./receiptpref"; export function useFoldModes(): FoldModes { return useSyncExternalStore(onFoldModesChange, foldModes, foldModes); @@ -11,20 +10,6 @@ export function useThinkingPreview(): boolean { return useSyncExternalStore(onFoldModesChange, showsThinkingPreview, showsThinkingPreview); } -/** The receipt preference of the kernel this pane talks to; a transcript drawn - * outside a pane reads the standalone one. */ -export const ReceiptPrefContext = createContext(receiptPrefOf()); - -export function useReceiptState(pref?: ReceiptPref): ReceiptState { - const own = useContext(ReceiptPrefContext); - const source = pref ?? own; - return useSyncExternalStore(source.subscribe, source.state, source.state); -} - -export function useShowsReceipt(pref?: ReceiptPref): boolean { - return useReceiptState(pref) === "on"; -} - /** Whether a part starts open under its mode, given whether it is still being * written, whether it failed and whether it changed a file. */ export function startsOpen(mode: FoldMode, live = false, failed = false, changed = false): boolean { diff --git a/desktop/frontend-next/src/state/receiptpref.ts b/desktop/frontend-next/src/state/receiptpref.ts deleted file mode 100644 index c98608e80c..0000000000 --- a/desktop/frontend-next/src/state/receiptpref.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { AgentPort } from "../port/port"; - -// Whether the delivery receipt is drawn as a card is the kernel's setting -// ([ui] show_receipt), so the terminal and every window answer alike. Each -// kernel a pane talks to has its own file, so the last answer is kept per port; -// until a kernel has answered nothing is drawn, and the record itself is kept -// in the session either way. -const LEGACY_RECEIPT_KEY = "rx-turn-receipt"; - -export type ReceiptState = "unknown" | "on" | "off"; - -export interface ReceiptPref { - state(): ReceiptState; - subscribe(fn: () => void): () => void; - adopt(state: Exclude): void; -} - -function createReceiptPref(initial: ReceiptState): ReceiptPref { - let state = initial; - const listeners = new Set<() => void>(); - return { - state: () => state, - subscribe: (fn) => { - listeners.add(fn); - return () => { - listeners.delete(fn); - }; - }, - adopt: (next) => { - if (state === next) return; - state = next; - listeners.forEach((fn) => fn()); - }, - }; -} - -const standalone = createReceiptPref("off"); -const perPort = new WeakMap(); - -/** The preference of the kernel behind this port. Without a port — a transcript - * drawn outside a pane — it is the standalone one, which starts off. */ -export function receiptPrefOf(port?: object): ReceiptPref { - if (!port) return standalone; - let pref = perPort.get(port); - if (!pref) { - pref = createReceiptPref("unknown"); - perPort.set(port, pref); - } - return pref; -} - -export const showsReceipt = (port?: object): boolean => receiptPrefOf(port).state() === "on"; - -type ReceiptPort = Pick; - -// Reads the kernel's setting. A choice this browser made before the setting -// moved belongs to this machine, so only this machine's own kernel (`local`) -// takes it over, once: the kernel gets the value only when it is "on" (off is -// its default), and the old key is dropped either way. -export async function loadReceiptPref(port: ReceiptPort, local: boolean): Promise { - const pref = receiptPrefOf(port); - let legacy: string | null = null; - if (local) { - try { - legacy = localStorage.getItem(LEGACY_RECEIPT_KEY); - } catch { - /* no storage: nothing to carry over */ - } - } - try { - let state = await port.receiptSettings(); - if (legacy === "on" && !state.show) state = await port.saveReceiptSettings(true); - pref.adopt(state.show ? "on" : "off"); - if (legacy !== null) { - try { - localStorage.removeItem(LEGACY_RECEIPT_KEY); - } catch { - /* the migration re-runs and finds the kernel already agreeing */ - } - } - } catch { - pref.adopt("off"); - } -} - -// Shows at once and asks the kernel to keep it; a refusal puts it back. -export async function saveReceiptPref(port: ReceiptPort, on: boolean): Promise { - const pref = receiptPrefOf(port); - const before = pref.state() === "on" ? "on" : "off"; - pref.adopt(on ? "on" : "off"); - try { - pref.adopt((await port.saveReceiptSettings(on)).show ? "on" : "off"); - } catch (e) { - pref.adopt(before); - throw e; - } -} diff --git a/desktop/frontend-next/src/state/session.test.ts b/desktop/frontend-next/src/state/session.test.ts index 7542f78f81..101bab68ab 100644 --- a/desktop/frontend-next/src/state/session.test.ts +++ b/desktop/frontend-next/src/state/session.test.ts @@ -2,25 +2,6 @@ import { describe, expect, it, vi } from "vitest"; import { chipLabel, fromHistory, initialState, reduce, type Item, type SessionEvent, type SessionState } from "./session"; import type { HistoryMessage } from "../port/port"; -// vitest runs these in Node, where there is no localStorage. The preference -// reader tolerates its absence by staying off, so a test that wants a receipt -// has to supply one. -const store = new Map(); -(globalThis as { localStorage?: unknown }).localStorage = { - getItem: (k: string) => store.get(k) ?? null, - setItem: (k: string, v: string) => void store.set(k, v), - removeItem: (k: string) => void store.delete(k), -}; -const showingReceipts = (body: () => T): T => body(); -const hidingReceipts = (body: () => T): T => { - store.set("rx-turn-receipt", "off"); - try { - return body(); - } finally { - store.delete("rx-turn-receipt"); - } -}; - const run = (evs: SessionEvent[]): SessionState => evs.reduce(reduce, initialState); describe("when this window saw the turn start", () => { @@ -306,39 +287,11 @@ describe("rebuilding a reopened transcript", () => { }); }); -describe("the turn's completion receipt", () => { - const receipts = (s: SessionState) => s.items.filter((i) => i.t === "receipt"); - const finish = (r: unknown): SessionEvent => ({ kind: "turn_done", receipt: r }) as SessionEvent; - - it("keeps the quality summary out of the transcript", () => { +describe("the turn's quality summary", () => { + it("stays out of the transcript", () => { const s = run([{ kind: "completion_summary", completion: { verdict: "partial", mutations: 2 } } as SessionEvent]); expect(s.items).toHaveLength(0); }); - - it("sources a clean verdict instead of asserting it", () => { - const s = showingReceipts(() => run([finish({ - verdict: "done", saysSomething: true, - changes: [{ path: "parser.go", reviewed: false }], - verifications: [{ command: "go test ./...", passed: true }], - })])); - expect(receipts(s)).toHaveLength(1); - }); - - it("shows the turn what it could not verify", () => { - const s = showingReceipts(() => run([finish({ - verdict: "partial", saysSomething: true, - gaps: [{ kind: "unreviewed_change", detail: "lexer.go" }], - })])); - expect(receipts(s)).toHaveLength(1); - }); - - // The kernel decides; the window does not second-guess it. A receipt with no - // gaps and no clean verdict says only that the host could not judge, which a - // transcript that already showed every step does not need repeated. - it("follows the kernel on what is worth a line", () => { - expect(receipts(run([finish({ verdict: "incomplete", saysSomething: false })]))).toHaveLength(0); - expect(receipts(run([done()]))).toHaveLength(0); - }); }); describe("a notice about the runtime rather than the conversation", () => { @@ -539,25 +492,6 @@ describe("repeated unclassified notices", () => { }); }); -describe("the turn's verification receipt", () => { - const done = (receipt: unknown): SessionEvent => ({ kind: "turn_done", receipt }) as SessionEvent; - const card = { saysSomething: true, verdict: "unproven", gaps: [{ kind: "unverified_change" }] }; - const receipts = (s: SessionState) => s.items.filter((i) => i.t === "receipt"); - - // The record is the transcript's; whether a card is drawn is decided at - // render time, so the preference never changes what the session holds. - it.each([["off", () => hidingReceipts(() => run([done(card)]))], ["default", () => run([done(card)])]])( - "closes the turn with the record when the preference is %s", - (_, go) => expect(receipts(go())).toHaveLength(1), - ); - - // The kernel still decides whether a receipt has content at all; the - // preference decides nothing about that. - it("still respects the kernel's own answer", () => { - expect(receipts(showingReceipts(() => run([done({ ...card, saysSomething: false })])))).toEqual([]); - }); -}); - // Nothing on the wire echoes what the user typed, so the row is the client's // to add — and its to take back when the line never left. Reporting the // refusal alone would leave a turn on screen that never happened. diff --git a/desktop/frontend-next/src/state/session.ts b/desktop/frontend-next/src/state/session.ts index 7d163b351e..09278d92d1 100644 --- a/desktop/frontend-next/src/state/session.ts +++ b/desktop/frontend-next/src/state/session.ts @@ -4,7 +4,7 @@ import { foldHistoryItems } from "./fold"; import { measureInteractions } from "./interactions"; import { foldExtension } from "./extension"; import { surfaceHistory } from "../ui/surface/call"; -import type { CostCoverage, Receipt, Tool, WireEvent } from "../port/wire"; +import type { CostCoverage, Tool, WireEvent } from "../port/wire"; import { noteTool, type Executions } from "./executions"; import { estimateTokens, sample } from "../port/tokens"; import type { HistoryMessage } from "../port/port"; @@ -620,7 +620,7 @@ function apply(s: SessionState, ev: SessionEvent): SessionState { doing: ev.outcome === "no_progress" ? "app.held" : ev.cancelled ? "common.cancelled" : ev.err ? "common.interrupted" : "common.done_2", waiting: {}, plan: livePlan(s.plan), - items: withReceipt([...sealTurn(sealSay(s.items, true, ev.cancelled ? "stopped" : ev.err && ev.outcome !== "no_progress" ? "failed" : undefined), ev.outcome === "no_progress" || ev.cancelled ? undefined : ev.err), ...worked(ev.workDurationMs, ev.workStartedAt)], ev.receipt), + items: [...sealTurn(sealSay(s.items, true, ev.cancelled ? "stopped" : ev.err && ev.outcome !== "no_progress" ? "failed" : undefined), ev.outcome === "no_progress" || ev.cancelled ? undefined : ev.err), ...worked(ev.workDurationMs, ev.workStartedAt)], }; default: @@ -641,14 +641,6 @@ function turnTerminal(ev: WireEvent): TurnTerminal { return { kind: "completed" }; } -// withReceipt appends the turn's completion record when the kernel says it has -// something to say. Whether the card is drawn is the reader's choice, made at -// render time, so the record stays in the transcript either way. -function withReceipt(items: Item[], r?: Receipt): Item[] { - if (!r?.saysSomething) return items; - return [...items, { t: "receipt", id: nextId(), r }]; -} - // The kernel wraps each user turn in control-plane blocks. They are // instructions to the model, not something the user said, so they never reach // the transcript. This list mirrors agent.TransientUserBlockTags by hand, and diff --git a/desktop/frontend-next/src/state/session_types.ts b/desktop/frontend-next/src/state/session_types.ts index 246246f0c4..018e0d824e 100644 --- a/desktop/frontend-next/src/state/session_types.ts +++ b/desktop/frontend-next/src/state/session_types.ts @@ -1,7 +1,7 @@ // What a session is made of, apart from the reducer that maintains it: the // rows the transcript draws and the state one turn hands the next. import type { StateTool } from "./outputcap"; -import type { Ask, Approval, Compaction, CostCoverage, ExtensionSurface, Guardian, Receipt, StoredAttachment, Via, WorkspaceLease } from "../port/wire"; +import type { Ask, Approval, Compaction, CostCoverage, ExtensionSurface, Guardian, StoredAttachment, Via, WorkspaceLease } from "../port/wire"; import type { Sample } from "../port/tokens"; import type { Executions } from "./executions"; @@ -53,7 +53,6 @@ export type Item = | { t: "ask"; id: string; ask: Ask; skipAt?: number; answered?: string[][]; answeredElsewhere?: boolean; timedOut?: boolean; by?: Via | "window"; said?: string; recorded?: string; surfaceValues?: Record; surfaceExpired?: boolean } | { t: "compaction"; id: string; c: Compaction; done: boolean } | { t: "remember"; id: string; m: RememberedFact; forgotten?: boolean } - | { t: "receipt"; id: string; r: Receipt } | { t: "worked"; id: string; ms: number; startedAt?: number } | { t: "extension"; id: string; ext: ExtensionSurface } // code identifies what the kernel is reporting. text is its own English, diff --git a/desktop/frontend-next/src/state/worked.test.ts b/desktop/frontend-next/src/state/worked.test.ts index 95aa8e5807..a42bbef5ad 100644 --- a/desktop/frontend-next/src/state/worked.test.ts +++ b/desktop/frontend-next/src/state/worked.test.ts @@ -5,9 +5,9 @@ import { clock, elapsed } from "../i18n/format"; const run = (done: object) => reduce(reduce(reduce(initialState, { kind: "turn_started" }), { kind: "message", text: "答复" }), { kind: "turn_done", ...done }); describe("the turn's run time", () => { - it("is drawn after the answer on the kernel's figure, before the receipt", () => { - const s = run({ workDurationMs: 65_000, receipt: { saysSomething: true, verdict: "done" } }); - expect(s.items.map((i) => i.t)).toEqual(["say", "worked", "receipt"]); + it("is drawn after the answer on the kernel's figure of the turn", () => { + const s = run({ workDurationMs: 65_000 }); + expect(s.items.map((i) => i.t)).toEqual(["say", "worked"]); expect(s.items[1]).toMatchObject({ t: "worked", ms: 65_000 }); }); diff --git a/desktop/frontend-next/src/styles/app.css b/desktop/frontend-next/src/styles/app.css index 4412c9f8b0..dc6aec4e3b 100644 --- a/desktop/frontend-next/src/styles/app.css +++ b/desktop/frontend-next/src/styles/app.css @@ -1223,19 +1223,6 @@ .chrome .goal[data-flash] { animation: flash 1.3s var(--ease-out) } .medit-r .n .r { color: var(--del) } - .guard { margin: 0; padding: 0 } - .enterbox[data-enter] .guard { animation: card var(--t-card) var(--ease-out) both } - .guard-hd { display: flex; align-items: baseline; gap: 8px } - .guard-hd .verdict { font: var(--w-med) var(--fs-ui) var(--ui); color: var(--text); flex: none } - .guard-hd .risk { margin-left: auto; font: var(--w-med) var(--fs-micro) var(--ui); color: var(--faint); white-space: nowrap } - .guard-hd .scope { font-size: var(--fs-ui); color: var(--muted); min-width: 0 } - .guard-hd .gauge { display: flex; gap: 2px; flex: none; align-self: center } - .guard-hd .gauge i { width: 3px; height: 9px; border-radius: var(--r-xs); background: var(--hair) } - .guard[data-risk="low"] .gauge i:nth-child(1) { background: var(--ok) } - .guard[data-risk="medium"] .gauge i:nth-child(-n+2) { background: var(--accent-ink) } - .guard[data-risk="high"] .gauge i { background: var(--err) } - .guard-why { margin-top: var(--y-close); font-size: var(--fs-ui); line-height: 1.62; color: var(--muted) } - .comp { margin-top: 7px } .comp-bar { height: 3px; background: color-mix(in srgb, var(--accent) 20%, var(--overlay)); overflow: hidden } .comp-bar i { display: block; height: 100%; background: var(--ok); transition: width var(--t-live) var(--ease-out) } @@ -2083,7 +2070,7 @@ .call, .nest, .ask, .hits > *, .steps > *, .term-l, .dl, .plan .s, .find, .ag, .file, .menu, .nest-ret, .hl .cost, .sym, .pip[data-settled], .plan .s[data-done] .b, - .rmark, .rmark path, .await i, .apv, .guard, .rrow, .shot, .intake { animation: none !important } + .rmark, .rmark path, .await i, .apv, .rrow, .shot, .intake { animation: none !important } .pane[data-intake] .compose { transform: none !important } .rmark path { stroke-dashoffset: 0 !important } diff --git a/desktop/frontend-next/src/styles/line.css b/desktop/frontend-next/src/styles/line.css index 779cb65054..b2417b99d1 100644 --- a/desktop/frontend-next/src/styles/line.css +++ b/desktop/frontend-next/src/styles/line.css @@ -1,7 +1,7 @@ .flow .line-row { margin: 0; padding: 4px 0 5px; border-bottom: 1px solid var(--hair); } .line-row .lr-main { display: flex; align-items: center; gap: 8px; min-height: 24px; } -.line-row .sym { flex: none; width: 14px; height: 14px; margin: 0; border: 0; border-radius: 0; background: none; box-shadow: none; color: var(--faint); } -.line-row .sym svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; } +.line-row .sym, .note-hd .sym { flex: none; width: 14px; height: 14px; margin: 0; border: 0; border-radius: 0; background: none; box-shadow: none; color: var(--faint); } +.line-row .sym svg, .note-hd .sym svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; } .line-row[data-lvl="ok"] .sym { color: var(--ok); } .line-row[data-lvl="warn"] .sym { color: var(--warn-ink); } .line-row[data-lvl="err"] .sym { color: var(--err-ink); } diff --git a/desktop/frontend-next/src/styles/studio.css b/desktop/frontend-next/src/styles/studio.css index 8673f7063f..875eacc31c 100644 --- a/desktop/frontend-next/src/styles/studio.css +++ b/desktop/frontend-next/src/styles/studio.css @@ -874,7 +874,6 @@ body { background: var(--page) !important; } .flow .call:is([data-k="host"], [data-k="mem"], [data-k="extension"]):not([data-k="me"]):not([data-k="say"]) > .c > .out .find { padding: 0; border-left: 0; } .flow .call:is([data-k="host"], [data-k="mem"], [data-k="extension"]):not([data-k="me"]):not([data-k="say"]) > .c > .out .find + .find { margin-top: 8px; } .flow .call:is([data-k="host"], [data-k="mem"], [data-k="extension"]):not([data-k="me"]):not([data-k="say"]) > .c > .out .find .t { font-weight: var(--w-reg); } -.flow .call:is([data-k="host"], [data-k="mem"], [data-k="extension"]):not([data-k="me"]):not([data-k="say"]) .guard-hd .verdict { font-weight: var(--w-reg); } .flow .call[data-prompt]:not([data-k="me"]):not([data-k="say"]) { display: block; position: relative; margin: 12px 0 !important; } .flow .call[data-prompt]:not([data-k="me"]):not([data-k="say"]) > .c { margin: 0 !important; } .flow .call:is([data-k="host"], [data-k="mem"], [data-k="extension"]):not([data-k="me"]):not([data-k="say"]) > .g, .flow .call[data-prompt]:not([data-k="me"]):not([data-k="say"]) > .g { position: absolute; top: 1px; left: 1px; z-index: 1; width: 34px; height: 38px; display: grid; place-items: center; pointer-events: none; } diff --git a/desktop/frontend-next/src/ui/AgentSettings.test.tsx b/desktop/frontend-next/src/ui/AgentSettings.test.tsx index dee3b428d5..1ecd6e0a99 100644 --- a/desktop/frontend-next/src/ui/AgentSettings.test.tsx +++ b/desktop/frontend-next/src/ui/AgentSettings.test.tsx @@ -13,7 +13,7 @@ function draw() { const go = vi.fn(); render( - [] }} persona={false} preset="自动" onChanged={() => {}} /> + [] }} persona={false} onChanged={() => {}} /> , ); return go; @@ -22,10 +22,10 @@ function draw() { it("is three cards of single rows, plan mode and the display switches living elsewhere", async () => { draw(); expect([...document.querySelectorAll(".prefs-card > header h3")].map((h) => h.textContent)).toEqual(["人设与预设", "计划与提问", "协作"]); - for (const id of ["persona", "agent-presets", "plan-approval", "ask-timeout", "turn-narration", "preset", "progress-watch", "session-ref", "subagent-claims", "peers"]) { + for (const id of ["persona", "agent-presets", "plan-approval", "ask-timeout", "turn-narration", "progress-watch", "session-ref", "subagent-claims", "peers"]) { await waitFor(() => expect(document.querySelector(`[data-setting="${id}"]`), id).toBeTruthy()); } - for (const id of ["plan-mode", "changed-files", "fold-work", "receipt", "feedback-level-notices"]) expect(document.querySelector(`[data-setting="${id}"]`), id).toBeNull(); + for (const id of ["plan-mode", "changed-files", "fold-work", "feedback-level-notices"]) expect(document.querySelector(`[data-setting="${id}"]`), id).toBeNull(); }); it("opens the presets page from both the persona row and the presets row", () => { diff --git a/desktop/frontend-next/src/ui/AgentSettings.tsx b/desktop/frontend-next/src/ui/AgentSettings.tsx index 4984b536a7..7964d67e08 100644 --- a/desktop/frontend-next/src/ui/AgentSettings.tsx +++ b/desktop/frontend-next/src/ui/AgentSettings.tsx @@ -10,7 +10,6 @@ import { PlanApproval } from "./PlanApproval"; import { ProgressWatch } from "./ProgressWatch"; import { SectionCard } from "./SectionCard"; import { SessionRef } from "./SessionRef"; -import { SettingRow } from "./SettingRow"; import { SettingDeep, SettingDeepContext } from "./SettingDeep"; import { SubagentClaims } from "./SubagentClaims"; import { TurnNarrationSetting } from "./TurnNarrationSetting"; @@ -19,11 +18,10 @@ interface Props { port: AgentPort; hub: Pick; persona: boolean; - preset: string; onChanged: () => void; } -export function AgentSettings({ port, hub, persona, preset, onChanged }: Props) { +export function AgentSettings({ port, hub, persona, onChanged }: Props) { const deep = useContext(SettingDeepContext); return ( <> @@ -46,9 +44,6 @@ export function AgentSettings({ port, hub, persona, preset, onChanged }: Props) - - {preset} - diff --git a/desktop/frontend-next/src/ui/ChatDisplaySettings.tsx b/desktop/frontend-next/src/ui/ChatDisplaySettings.tsx index 4d67eadf17..e0c38a99c8 100644 --- a/desktop/frontend-next/src/ui/ChatDisplaySettings.tsx +++ b/desktop/frontend-next/src/ui/ChatDisplaySettings.tsx @@ -9,7 +9,6 @@ import { NewAnswerSection } from "./NewAnswerSection"; import { OfficePreviewSection } from "./OfficePreviewSection"; import { PlanProgressSection } from "./PlanProgressSection"; import { PresentFiles } from "./PresentFiles"; -import { ReceiptSetting } from "./ReceiptSetting"; import { SectionCard } from "./SectionCard"; import { SettingDeep } from "./SettingDeep"; @@ -29,7 +28,6 @@ export function ChatDisplaySettings({ port }: { port: AgentPort }) { - diff --git a/desktop/frontend-next/src/ui/LookFamilyPages.test.tsx b/desktop/frontend-next/src/ui/LookFamilyPages.test.tsx index 8836f1c76c..d44f1d29b7 100644 --- a/desktop/frontend-next/src/ui/LookFamilyPages.test.tsx +++ b/desktop/frontend-next/src/ui/LookFamilyPages.test.tsx @@ -32,7 +32,7 @@ it("conversation display is process, files and previews, and notices", async () const port = new MockPort() as unknown as AgentPort; frame(); expect(titles()).toEqual(["过程", "文件与预览", "提示"]); - for (const id of ["fold-work", "folding", "plan-progress", "changed-files", "present-files", "office-preview", "receipt", "new-answer-marker", "feedback-level-notices"]) { + for (const id of ["fold-work", "folding", "plan-progress", "changed-files", "present-files", "office-preview", "new-answer-marker", "feedback-level-notices"]) { await waitFor(() => expect(has(id), id).toBe(true)); } }); diff --git a/desktop/frontend-next/src/ui/Pane.tsx b/desktop/frontend-next/src/ui/Pane.tsx index 39805b83f5..edd129a29a 100644 --- a/desktop/frontend-next/src/ui/Pane.tsx +++ b/desktop/frontend-next/src/ui/Pane.tsx @@ -1,7 +1,5 @@ import { memo, useCallback, useEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react"; import { streamBudget } from "../port/streambudget"; -import { loadReceiptPref, receiptPrefOf } from "../state/receiptpref"; -import { ReceiptPrefContext } from "../state/foldpref"; import { moneyList } from "../i18n/format"; import { pushToast } from "../state/toasts"; import { reason } from "../i18n/kernel"; @@ -203,8 +201,6 @@ function PaneView({ port, hub, rt, title, active, visible, sideHost, side, onFoc const [wallet, refreshWallet] = useWallet(port, accountOf(status?.modelRef)); const hideAmounts = useHidesAmounts(); - useEffect(() => { void loadReceiptPref(port, !rt.host); }, [port, rt.host]); - const revalue = useCallback(() => { port.status().then((st) => { applyStatus(st); dispatch(totalsOf(st) as never); }).catch(() => {}); refreshWallet(); @@ -437,8 +433,7 @@ function PaneView({ port, hub, rt, title, active, visible, sideHost, side, onFoc onRevise: () => setAskFocus((n) => n + 1), }); - const receiptPref = receiptPrefOf(port); - const find = useFind(s.items, findPulse, active, useCallback(() => showView("flow"), [showView]), receiptPref, findTogglePulse); + const find = useFind(s.items, findPulse, active, useCallback(() => showView("flow"), [showView]), findTogglePulse); useHistorySearch(historyPaging, find.open, find.query); const onRunDetail = useCallback(() => showView("analysis"), [showView]); @@ -523,7 +518,6 @@ function PaneView({ port, hub, rt, title, active, visible, sideHost, side, onFoc