mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55c3d81bbc | ||
|
|
ffd73e115e | ||
|
|
1a33ce5692 | ||
|
|
6e2ed58a34 | ||
|
|
2be1dba2da | ||
|
|
a717691db9 | ||
|
|
dfdbfbd4d8 | ||
|
|
7b7af1a78c | ||
|
|
59526e5156 | ||
|
|
935200d9e8 | ||
|
|
9006ff804c | ||
|
|
c57cf43aeb | ||
|
|
a9e2a12927 | ||
|
|
97dee44a3d | ||
|
|
2192bfb20c | ||
|
|
5516d5a7d8 | ||
|
|
d10871190c | ||
|
|
ccdd8a721f |
No files matched your search
@@ -23,7 +23,7 @@ try {
|
||||
const tb = await trigger.boundingBox();
|
||||
await page.mouse.move(tb.x + tb.width / 2, tb.y + tb.height / 2);
|
||||
await page.waitForTimeout(250);
|
||||
const card = page.locator(".chrome-context-card").first();
|
||||
const card = page.locator(".studio-context-anchor .chrome-context-card");
|
||||
check(`${scheme}: the card opens while the pointer is on the meter`, await card.isVisible());
|
||||
const target = card.locator("button, a").first();
|
||||
const cb = await target.boundingBox();
|
||||
|
||||
@@ -1528,6 +1528,9 @@ export const EN: Record<string, string> = {
|
||||
"待发送已暂停,这条消息已排入队列,点“继续派发”后才会发送": "Sending is paused. This message is queued and goes out once you press Release",
|
||||
"引导没有生效:这一轮在处理它之前就结束了。如果仍然需要,请再发送一次:": "Guidance was not applied because the turn ended before it could be processed. Send it again if it is still needed:",
|
||||
"费用显示币种已设为 {mode}": "Cost display currency set to {mode}",
|
||||
"这个会话保存的压缩结果没有被复用:它所总结的历史之后发生了变化,现在发送的是完整历史,可能超出模型容量": "This conversation's saved compaction was not reused: the history it summarised has changed since. The full history is being sent, which may exceed the model's window.",
|
||||
"这个会话保存的压缩结果是在另一个模型或工作区下生成的,与当前历史对不上,没有被复用,现在发送的是完整历史": "This conversation's saved compaction was made under another model or workspace and does not match its history, so it was not reused. The full history is being sent.",
|
||||
"这些工作区在同一个 Git 仓库 {repo} 内,同一仓库共用一个写入域;把项目移出该仓库,或在用户配置里设 write_lease = \"optimistic\",才能并行写入": "These workspaces are inside the same Git repository, {repo}, and one repository is one write domain; move the project out of it, or set write_lease = \"optimistic\" in your user config, to write in parallel",
|
||||
"配置文件里的提问等待时长无效(须为 0 或 10 到 86400 之间的整数),提问现在会一直等你回答;到设置里重新设一个": "The question wait time in the config file is not valid (it must be a whole number, 0 or 10 to 86400), so questions wait for you as long as it takes; set it again in Settings.",
|
||||
"模型卡在重复输出同一段文字,这一轮已停止;可以重试、补充引导,或换一个供应商/模型": "The model is stuck repeating the same text; this turn was stopped. Try again, add guidance, or switch provider/model.",
|
||||
"扩展 {ext} 的配套后台程序没有运行,该扩展本次(在 {point})已被跳过;到「工具与集成」里查看并启动它,或停用该扩展": "The companion background program of extension {ext} is not running, so the extension was skipped this time (at {point}). Open Tools and integrations to check and start it, or disable the extension.",
|
||||
|
||||
@@ -771,6 +771,8 @@ export const EN_SETTINGS: Record<string, string> = {
|
||||
"价格未知": "Price unknown",
|
||||
"还没有计费的请求": "Nothing billed yet",
|
||||
"会话概览": "Session overview",
|
||||
"下一次请求会带上的内容,是当前值,不是累计": "What the next request would carry: a current reading, not a running total",
|
||||
"自动压缩于 {n}": "Folds automatically at {n}",
|
||||
"自本次打开此会话起;恢复或分叉会重新计数": "Since this session was opened in this run; resuming or forking counts again",
|
||||
"这个会话还没有计费的请求": "This session has no billed requests yet",
|
||||
"返回了用量与价格的模型响应": "Model responses that reported usage and a price",
|
||||
|
||||
@@ -46,5 +46,7 @@ export const NOTICE_TEXT: Record<string, string> = {
|
||||
permission_rules_dormant: "有 {n} 条权限规则没有对应的工具,匹配不到任何调用,因此起不到限制作用(如「{list}」里的 {rule});到「设置 → 权限」里删除或改写,命令要写成 Bash(命令:*)",
|
||||
extension_skipped: "扩展 {ext} 的配套后台程序没有运行,该扩展本次(在 {point})已被跳过;到「工具与集成」里查看并启动它,或停用该扩展",
|
||||
ask_timeout_invalid: "配置文件里的提问等待时长无效(须为 0 或 10 到 86400 之间的整数),提问现在会一直等你回答;到设置里重新设一个",
|
||||
fold_not_reused_history: "这个会话保存的压缩结果没有被复用:它所总结的历史之后发生了变化,现在发送的是完整历史,可能超出模型容量",
|
||||
fold_not_reused_lineage: "这个会话保存的压缩结果是在另一个模型或工作区下生成的,与当前历史对不上,没有被复用,现在发送的是完整历史",
|
||||
perseveration_loop: "模型卡在重复输出同一段文字,这一轮已停止;可以重试、补充引导,或换一个供应商/模型",
|
||||
};
|
||||
@@ -22,4 +22,18 @@ describe("write claim attribution", () => {
|
||||
expect(say({ code: "workspace.write_conflict" }, "")).not.toBe("");
|
||||
});
|
||||
}
|
||||
|
||||
for (const language of ["zh", "en"]) {
|
||||
it(`says a repository folded the folders into one write domain, with the way out, in ${language}`, () => {
|
||||
localStorage.setItem(STORAGE, language);
|
||||
boot();
|
||||
const folded = { contended: 0, heldMs: 0, idleMs: 0, holder: "A", holderSessionId: "s", paths: ["d:/home/ws"], requestedPaths: ["d:/home/ws"], repository: "d:/home/ws" };
|
||||
const detail = workspaceLeaseDetail(folded);
|
||||
expect(detail).toContain("d:/home/ws");
|
||||
expect(detail).toContain(language === "en" ? "same Git repository" : "同一个 Git 仓库");
|
||||
expect(detail).toContain("write_lease");
|
||||
expect(workspaceLeaseDetail({ ...folded, repository: undefined })).not.toContain("write_lease");
|
||||
expect(workspaceLeaseDetail(folded, false)).not.toContain("write_lease");
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -4,5 +4,6 @@ import { t } from "./index";
|
||||
export function workspaceLeaseDetail(claim: WorkspaceLease, showHolder = true): string {
|
||||
return [showHolder && (claim.holder || claim.holderSessionId) ? t("会话 {holder}({session})持有写入范围:{paths}", {
|
||||
holder: claim.holder ?? "", session: claim.holderSessionId ?? "", paths: JSON.stringify(claim.paths ?? []),
|
||||
}) : "", claim.requestedPaths?.length ? t("所需写入范围:{paths}", { paths: JSON.stringify(claim.requestedPaths) }) : ""].filter(Boolean).join(" · ");
|
||||
}) : "", claim.requestedPaths?.length ? t("所需写入范围:{paths}", { paths: JSON.stringify(claim.requestedPaths) }) : "",
|
||||
showHolder && claim.repository ? t("这些工作区在同一个 Git 仓库 {repo} 内,同一仓库共用一个写入域;把项目移出该仓库,或在用户配置里设 write_lease = \"optimistic\",才能并行写入", { repo: claim.repository }) : ""].filter(Boolean).join(" · ");
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { ContextComposition } from "./context_composition";
|
||||
import type { WorkspaceLease } from "./workspace_lease";
|
||||
export type { WorkspaceLease } from "./workspace_lease";
|
||||
import type { SurfaceImage, SurfaceLocation, SurfaceFact, SurfaceDataset } from "./surface-rich";
|
||||
export type { SurfaceImage, SurfaceLocation, SurfaceFact, SurfaceDataset, SurfaceSeries } from "./surface-rich";
|
||||
// Mirrors internal/contract/eventwire. Field names and kind strings must match exactly;
|
||||
@@ -477,21 +479,6 @@ export interface TodoProgress {
|
||||
progressRevision?: number;
|
||||
}
|
||||
|
||||
/** One closed account of the workspace write lease. Waits under the notice
|
||||
* grace are counted here and nowhere else, and idleMs is the stretch held
|
||||
* after the last write asked for it. */
|
||||
export interface WorkspaceLease {
|
||||
contended: number;
|
||||
reported?: number;
|
||||
waitedMs?: number;
|
||||
heldMs: number;
|
||||
idleMs: number;
|
||||
holder?: string;
|
||||
holderSessionId?: string;
|
||||
paths?: string[];
|
||||
requestedPaths?: string[];
|
||||
}
|
||||
|
||||
// One context-maintenance transaction. status "noop" carries code: the attempt
|
||||
// ran and freed nothing, and code is what tells "already folded this turn" from
|
||||
// "nothing left to fold" — they read alike and mean opposite things about what
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/** One closed account of the workspace write lease. Waits under the notice
|
||||
* grace are counted here and nowhere else, and idleMs is the stretch held
|
||||
* after the last write asked for it. */
|
||||
export interface WorkspaceLease {
|
||||
contended: number;
|
||||
reported?: number;
|
||||
waitedMs?: number;
|
||||
heldMs: number;
|
||||
idleMs: number;
|
||||
holder?: string;
|
||||
holderSessionId?: string;
|
||||
paths?: string[];
|
||||
requestedPaths?: string[];
|
||||
/** The git repository root that folded this session's folder into the shared write domain. */
|
||||
repository?: string;
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
.studio-meterrail .studio-overview-anchor > button:hover { background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); }
|
||||
:root[data-theme="light"] .studio-meterrail .studio-overview-anchor > button { color: var(--ghost); }
|
||||
.studio-overview-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 14px; margin: 0; }
|
||||
.studio-overview-grid > div { display: grid; gap: 2px; min-width: 0; }
|
||||
.studio-overview-grid > div { display: grid; gap: 2px; align-content: start; min-width: 0; }
|
||||
.studio-overview-grid dt { color: var(--ghost); font: 500 9px/1.3 var(--ui); }
|
||||
.studio-overview-grid dd { margin: 0; color: var(--text); font: 600 13px/1.2 var(--mono); }
|
||||
.studio-overview-grid small { color: var(--ghost); font: 500 9px/1.4 var(--ui); }
|
||||
@@ -24,4 +24,5 @@
|
||||
.studio-overview-row > small { text-align: right; font-family: var(--mono); }
|
||||
@media (hover: hover) {
|
||||
.studio-overview-anchor:hover .studio-overview-popover { opacity: 1; visibility: visible; pointer-events: auto; transform: none; transition-delay: 0s; }
|
||||
.studio-meterrail:has(.studio-context-anchor[data-open]) .studio-overview-anchor:not([data-open]):hover .studio-overview-popover { opacity: 0; visibility: hidden; pointer-events: none; }
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import userEvent from "@testing-library/user-event";
|
||||
import "./testkit";
|
||||
import { MeterRail } from "./MeterRail";
|
||||
import { boot, STORAGE } from "../i18n";
|
||||
import type { SessionUsage } from "../port/port";
|
||||
import type { ContextBreakdown, SessionUsage } from "../port/port";
|
||||
import type { CostQuote } from "../port/wire";
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -24,11 +24,13 @@ const usage: SessionUsage = {
|
||||
],
|
||||
};
|
||||
|
||||
function rail(over: { usage?: SessionUsage; quote?: CostQuote; hideAmounts?: boolean } = {}) {
|
||||
const context = { used: 42_000, window: 128_000, system: 3000, tools: 9000, user: 6000, reply: 12000, output: 12000, compact_at: 96_000, boundary: "window", capacity_at: 128_000 } as ContextBreakdown;
|
||||
|
||||
function rail(over: { usage?: SessionUsage; quote?: CostQuote; hideAmounts?: boolean; ctx?: ContextBreakdown | null } = {}) {
|
||||
return render(
|
||||
<MeterRail
|
||||
tps={0} trail={[]} running={false} speed={{ output: 0, modelSeconds: 0, average: 0, firstToken: 0 }}
|
||||
metrics={{ hit: 0, miss: 0 }} ctx={null} mcp={[]} compact={{ phase: "idle" } as never} cost="" wallet={{} as never}
|
||||
metrics={{ hit: 0, miss: 0 }} ctx={"ctx" in over ? (over.ctx ?? null) : context} mcp={[]} compact={{ phase: "idle" } as never} cost="" wallet={{} as never}
|
||||
hideAmounts={over.hideAmounts ?? false} tasks={[]} jobs={[]} onSettings={vi.fn()} onCancelJob={vi.fn(async () => {})}
|
||||
usage={"usage" in over ? over.usage : usage} quote={"quote" in over ? over.quote : quote()}
|
||||
/>,
|
||||
@@ -119,4 +121,39 @@ describe("the session overview behind the cache meter", () => {
|
||||
rail();
|
||||
expect(button().textContent).toContain("77%");
|
||||
});
|
||||
|
||||
it("shows the context window in the same card, as the next request's content and not a running total", () => {
|
||||
rail();
|
||||
const section = within(card()).getByRole("region", { name: "上下文窗口" });
|
||||
expect(section.textContent).toContain("42k");
|
||||
expect(section.textContent).toContain("128k");
|
||||
expect(section.textContent).toContain("33%");
|
||||
expect(section.textContent).toContain("下一次请求会带上的内容,是当前值,不是累计");
|
||||
expect(section.querySelectorAll("[data-part]").length).toBe(5);
|
||||
});
|
||||
|
||||
it("states the automatic fold line only when the kernel has one", () => {
|
||||
rail();
|
||||
expect(within(card()).getByRole("region", { name: "上下文窗口" }).textContent).toContain("自动压缩于 96k");
|
||||
cleanup();
|
||||
rail({ ctx: { ...context, compact_at: 0 } });
|
||||
expect(within(card()).getByRole("region", { name: "上下文窗口" }).textContent).not.toContain("自动压缩于");
|
||||
});
|
||||
|
||||
it("keeps the window section when no request was billed, and omits it when the kernel sent none", () => {
|
||||
rail({ usage: { ...usage, requests: 0, sources: [] } });
|
||||
expect(within(card()).getByRole("region", { name: "上下文窗口" })).toBeTruthy();
|
||||
cleanup();
|
||||
rail({ ctx: null });
|
||||
expect(within(card()).queryByRole("region", { name: "上下文窗口" })).toBeNull();
|
||||
});
|
||||
|
||||
it("hands over to the context details without leaving both cards open", async () => {
|
||||
const user = userEvent.setup();
|
||||
rail();
|
||||
await user.click(button());
|
||||
await user.click(within(card()).getByRole("button", { name: "上下文详情" }));
|
||||
expect(button().getAttribute("aria-expanded")).toBe("false");
|
||||
expect(screen.getByRole("button", { name: "查看上下文与压缩" }).getAttribute("aria-expanded")).toBe("true");
|
||||
});
|
||||
});
|
||||
@@ -79,7 +79,7 @@ export function MeterRail({ tps, trail, running, speed, metrics, ctx, mcp, compa
|
||||
<button className="studio-meter-cache" type="button" data-action="metrics.details" data-value="overview" aria-expanded={overviewOpen} aria-haspopup="dialog" aria-label={t("查看会话概览")} title={cacheRate === null ? t("尚无缓存数据") : t("命中 {hit} · 未命中 {miss}", { hit: metrics.hit.toLocaleString(), miss: metrics.miss.toLocaleString() })} onClick={() => { setMeterOpen(false); setOverviewOpen((open) => !open); }}>
|
||||
<span>{t("缓存")}</span><b>{cacheRate === null ? "—" : `${cacheRate}%`}</b>
|
||||
</button>
|
||||
<SessionOverviewCard className="studio-overview-popover" usage={usage} quote={quote} hideAmounts={hideAmounts} />
|
||||
<SessionOverviewCard className="studio-overview-popover" usage={usage} quote={quote} ctx={ctx} hideAmounts={hideAmounts} onContext={ctx && ctx.window > 0 ? () => { setOverviewOpen(false); setMeterOpen(true); } : undefined} />
|
||||
</div>
|
||||
{ctx && ctx.window > 0 && (
|
||||
<div className="studio-context-anchor" data-open={meterOpen ? "" : undefined}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "../i18n";
|
||||
import { money } from "../i18n/format";
|
||||
import type { ContextBreakdown } from "../port/shell";
|
||||
import type { SessionUsage } from "../port/session";
|
||||
import type { CostQuote } from "../port/wire";
|
||||
import { cacheRate, costSay, sessionTokens, sourceTokens, type CostSay } from "../state/overview";
|
||||
@@ -47,8 +48,10 @@ function cost(say: CostSay, hide: boolean): { text: string; note: string } {
|
||||
// The session as the kernel's ledger holds it, each figure beside what it counts.
|
||||
// Everything here is since this session was opened in this run: a resumed or
|
||||
// forked session counts again from the start.
|
||||
export function SessionOverviewCard({ usage, quote, hideAmounts, className = "" }: {
|
||||
export function SessionOverviewCard({ usage, quote, ctx, hideAmounts, onContext, className = "" }: {
|
||||
usage?: SessionUsage;
|
||||
ctx?: ContextBreakdown | null;
|
||||
onContext?: () => void;
|
||||
quote?: CostQuote;
|
||||
hideAmounts: boolean;
|
||||
className?: string;
|
||||
@@ -89,6 +92,23 @@ export function SessionOverviewCard({ usage, quote, hideAmounts, className = ""
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
{ctx && ctx.window > 0 && <ContextWindow ctx={ctx} onContext={onContext} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PARTS = [["sys", "system"], ["tool", "tools"], ["user", "user"], ["reply", "reply"], ["output", "output"]] as const;
|
||||
|
||||
// The window is a present reading, not a running total: what the next request would carry.
|
||||
function ContextWindow({ ctx, onContext }: { ctx: ContextBreakdown; onContext?: () => void }) {
|
||||
return (
|
||||
<section aria-label={t("上下文窗口")}>
|
||||
<div><b>{t("上下文窗口")}</b><b>{Math.min(100, Math.round((ctx.used / ctx.window) * 100))}%</b></div>
|
||||
<small>{t("下一次请求会带上的内容,是当前值,不是累计")}</small>
|
||||
<div className="chrome-context-total"><span>{t("已使用")} <b>{tokens(ctx.used)}</b></span><span>{t("模型容量")} <b>{tokens(ctx.window)}</b></span></div>
|
||||
<div className="chrome-context-bar">{PARTS.map(([key, field]) => <i key={key} data-part={key} style={{ width: `${ctx.used ? Math.max(1, (ctx[field] / ctx.used) * 100) : 0}%` }} />)}</div>
|
||||
{ctx.compact_at > 0 && <small>{t("自动压缩于 {n}", { n: tokens(ctx.compact_at) })}</small>}
|
||||
{onContext && <button type="button" data-action="metrics.details" data-value="context-from-overview" onClick={onContext}>{t("上下文详情")}</button>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -116,5 +116,5 @@ func workspaceLeaseNotice(w workspacelease.Wait) event.Event {
|
||||
|
||||
func workspaceLeaseScope(w workspacelease.Wait) *event.WorkspaceLease {
|
||||
return &event.WorkspaceLease{Holder: w.Holder, HolderSessionID: w.HolderSessionID,
|
||||
Paths: w.Paths, RequestedPaths: w.RequestedPaths}
|
||||
Paths: w.Paths, RequestedPaths: w.RequestedPaths, Repository: w.Repository}
|
||||
}
|
||||
@@ -105,3 +105,10 @@ const (
|
||||
NoticeCodeWebSearchModelUnsupported = "web_search_model_unsupported"
|
||||
NoticeCodeWebSearchModelNoCredentials = "web_search_model_no_credentials"
|
||||
)
|
||||
|
||||
// A saved fold was not reused when its session was bound: the whole history is
|
||||
// sent instead.
|
||||
const (
|
||||
NoticeCodeFoldNotReusedHistory = "fold_not_reused_history"
|
||||
NoticeCodeFoldNotReusedLineage = "fold_not_reused_lineage"
|
||||
)
|
||||
@@ -13,4 +13,5 @@ type WorkspaceLease struct {
|
||||
HolderSessionID string `json:"holderSessionId,omitempty"`
|
||||
Paths []string `json:"paths,omitempty"`
|
||||
RequestedPaths []string `json:"requestedPaths,omitempty"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
}
|
||||
@@ -13,6 +13,7 @@ type WorkspaceLease struct {
|
||||
HolderSessionID string `json:"holderSessionId,omitempty"`
|
||||
Paths []string `json:"paths,omitempty"`
|
||||
RequestedPaths []string `json:"requestedPaths,omitempty"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
}
|
||||
|
||||
func toWireWorkspaceLease(l *event.WorkspaceLease) *WorkspaceLease {
|
||||
@@ -23,7 +24,7 @@ func toWireWorkspaceLease(l *event.WorkspaceLease) *WorkspaceLease {
|
||||
Contended: l.Contended, Reported: l.Reported,
|
||||
WaitedMs: l.WaitedMs, HeldMs: l.HeldMs, IdleMs: l.IdleMs,
|
||||
Holder: l.Holder, HolderSessionID: l.HolderSessionID,
|
||||
Paths: l.Paths, RequestedPaths: l.RequestedPaths,
|
||||
Paths: l.Paths, RequestedPaths: l.RequestedPaths, Repository: l.Repository,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -210,6 +210,9 @@ func (a *contextWindow) loadProjectionSidecar(sessionPath string) {
|
||||
if (key != "" && !keyOK) || !hasMaintenanceSignal {
|
||||
a.sess.win.compactionState = sessionstore.CompactionState{}
|
||||
a.sess.win.checkpointState = "none"
|
||||
if key != "" && !keyOK && len(st.Projection.Messages) > 0 {
|
||||
a.sess.win.checkpointState = string(ProjectionDropLineage)
|
||||
}
|
||||
a.sess.win.compactionMu.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -229,12 +232,15 @@ func (a *contextWindow) loadProjectionSidecar(sessionPath string) {
|
||||
fingerprint = a.prefixHasher(rewriteVersion)
|
||||
}
|
||||
valid := len(st.Projection.Messages) > 0 && projectionValid(st, msgs, key, fingerprint)
|
||||
if !valid && len(st.Projection.Messages) > 0 {
|
||||
dropped := !valid && len(st.Projection.Messages) > 0
|
||||
if dropped {
|
||||
// Keep blocked receipts / telemetry; drop unusable projection body.
|
||||
st.Projection = sessionstore.ContextProjection{}
|
||||
}
|
||||
a.sess.win.compactionState = st
|
||||
if valid {
|
||||
if dropped {
|
||||
a.sess.win.checkpointState = string(ProjectionDropHistory)
|
||||
} else if valid {
|
||||
a.sess.win.checkpointState = "restored"
|
||||
if needsNormalization {
|
||||
if err := a.persistCompactionStateLocked(); err != nil {
|
||||
@@ -247,6 +253,37 @@ func (a *contextWindow) loadProjectionSidecar(sessionPath string) {
|
||||
a.sess.win.compactionMu.Unlock()
|
||||
}
|
||||
|
||||
// ProjectionDrop names why a saved fold was not reused when its session was
|
||||
// bound. The host is the only party that saw which it was.
|
||||
type ProjectionDrop string
|
||||
|
||||
const (
|
||||
ProjectionDropNone ProjectionDrop = ""
|
||||
// ProjectionDropHistory: the fold's lineage matched but the history behind it changed.
|
||||
ProjectionDropHistory ProjectionDrop = "dropped_history"
|
||||
// ProjectionDropLineage: another model or workspace made it and the history does not match either.
|
||||
ProjectionDropLineage ProjectionDrop = "dropped_lineage"
|
||||
)
|
||||
|
||||
// ProjectionDropped reports why the fold saved beside the bound session was
|
||||
// not reused, or ProjectionDropNone when there was none or it was.
|
||||
func (a *Agent) ProjectionDropped() ProjectionDrop {
|
||||
if a == nil {
|
||||
return ProjectionDropNone
|
||||
}
|
||||
return a.window().projectionDropped()
|
||||
}
|
||||
|
||||
func (a *contextWindow) projectionDropped() ProjectionDrop {
|
||||
a.sess.win.compactionMu.Lock()
|
||||
defer a.sess.win.compactionMu.Unlock()
|
||||
switch d := ProjectionDrop(a.sess.win.checkpointState); d {
|
||||
case ProjectionDropHistory, ProjectionDropLineage:
|
||||
return d
|
||||
}
|
||||
return ProjectionDropNone
|
||||
}
|
||||
|
||||
// lineageKeyCompatible reports whether a stored PromptCacheKey still belongs to
|
||||
// the current session/model lineage. Legacy native context-editing keys used a
|
||||
// "|context-editing-native-..." suffix on an otherwise matching base key.
|
||||
|
||||
@@ -321,3 +321,62 @@ func TestCompactInstallsCoveredPrefixHash(t *testing.T) {
|
||||
t.Fatal("fresh projection should validate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadProjectionSidecarSaysWhyAFoldWasNotReused(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
key string
|
||||
covers []provider.Message
|
||||
want ProjectionDrop
|
||||
}{
|
||||
{"history rewritten under the same lineage", "", []provider.Message{{Role: provider.RoleSystem, Content: "sys-old"}}, ProjectionDropHistory},
|
||||
{"history rewritten under another lineage", "ws|s|other-model", []provider.Message{{Role: provider.RoleSystem, Content: "sys-old"}}, ProjectionDropLineage},
|
||||
{"still valid", "", []provider.Message{{Role: provider.RoleSystem, Content: "sys"}}, ProjectionDropNone},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
path := filepath.Join(testenv.TempDir(t), "s.jsonl")
|
||||
key := tc.key
|
||||
if key == "" {
|
||||
key = promptCacheKey("ws", sessionstore.BranchID(path), "this-model")
|
||||
}
|
||||
if err := sessionstore.SaveCompactionState(path, sessionstore.CompactionState{
|
||||
SchemaVersion: sessionstore.CompactionStateSchemaV1,
|
||||
PromptCacheKey: key,
|
||||
Projection: sessionstore.ContextProjection{
|
||||
Messages: []provider.Message{{Role: provider.RoleSystem, Content: "summary"}},
|
||||
CoveredCount: 1,
|
||||
CoveredPrefixHash: sessionstore.CoveredPrefixHash(tc.covers, 1),
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := New(nil, nil, sessionstore.NewSession("sys"), Options{SessionPath: path, WorkspaceID: "ws", ModelRef: "this-model"}, event.Discard)
|
||||
if got := a.ProjectionDropped(); got != tc.want {
|
||||
t.Fatalf("ProjectionDropped = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectionDroppedIsClearedByANewBinding(t *testing.T) {
|
||||
path := filepath.Join(testenv.TempDir(t), "s.jsonl")
|
||||
if err := sessionstore.SaveCompactionState(path, sessionstore.CompactionState{
|
||||
SchemaVersion: sessionstore.CompactionStateSchemaV1,
|
||||
PromptCacheKey: promptCacheKey("ws", sessionstore.BranchID(path), "m"),
|
||||
Projection: sessionstore.ContextProjection{
|
||||
Messages: []provider.Message{{Role: provider.RoleSystem, Content: "summary"}}, CoveredCount: 1,
|
||||
CoveredPrefixHash: sessionstore.CoveredPrefixHash([]provider.Message{{Role: provider.RoleSystem, Content: "other"}}, 1),
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := New(nil, nil, sessionstore.NewSession("sys"), Options{SessionPath: path, WorkspaceID: "ws", ModelRef: "m"}, event.Discard)
|
||||
if a.ProjectionDropped() != ProjectionDropHistory {
|
||||
t.Fatalf("setup: ProjectionDropped = %q", a.ProjectionDropped())
|
||||
}
|
||||
a.BindSessionPath(filepath.Join(testenv.TempDir(t), "fresh.jsonl"), true)
|
||||
if got := a.ProjectionDropped(); got != ProjectionDropNone {
|
||||
t.Fatalf("a session with no sidecar still reports %q", got)
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func workspaceLeaseConflictScope(err error) *event.WorkspaceLease {
|
||||
return nil
|
||||
}
|
||||
return &event.WorkspaceLease{Holder: conflict.Holder, HolderSessionID: conflict.SessionID,
|
||||
Paths: conflict.Paths, RequestedPaths: conflict.RequestedPaths}
|
||||
Paths: conflict.Paths, RequestedPaths: conflict.RequestedPaths, Repository: conflict.Repository}
|
||||
}
|
||||
|
||||
func workspaceLeaseRefusalCode(err error) string {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/contract/provider"
|
||||
"reasonix/internal/runtime/agent"
|
||||
"reasonix/internal/state/sessionstore"
|
||||
)
|
||||
|
||||
func foldNoticeCodes(events []event.Event) []string {
|
||||
var out []string
|
||||
for _, e := range events {
|
||||
if e.Kind == event.Notice && (e.Code == event.NoticeCodeFoldNotReusedHistory || e.Code == event.NoticeCodeFoldNotReusedLineage) {
|
||||
out = append(out, e.Code)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func foldController(t *testing.T, sink event.Sink, coversHash string) (*Controller, *sessionstore.Session, string) {
|
||||
t.Helper()
|
||||
dir := testenv.TempDir(t)
|
||||
saved := sessionstore.NewSession("sys")
|
||||
saved.Add(provider.Message{Role: provider.RoleUser, Content: "task"})
|
||||
path := sessionstore.NewSessionPath(dir, "test")
|
||||
if err := saved.Save(path); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
if _, err := sessionstore.EnsureBranchMeta(path); err != nil {
|
||||
t.Fatalf("meta: %v", err)
|
||||
}
|
||||
if err := sessionstore.SaveCompactionState(path, sessionstore.CompactionState{
|
||||
SchemaVersion: sessionstore.CompactionStateSchemaV1,
|
||||
Projection: sessionstore.ContextProjection{
|
||||
Messages: []provider.Message{{Role: provider.RoleSystem, Content: "summary"}},
|
||||
CoveredCount: 2,
|
||||
CoveredPrefixHash: coversHash,
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("sidecar: %v", err)
|
||||
}
|
||||
exec := agent.New(nil, nil, sessionstore.NewSession("sys"), agent.Options{ContextWindow: 1000, RecentKeep: 2, ArchiveDir: dir}, event.Discard)
|
||||
c := New(Options{Executor: exec, SessionDir: dir, Label: "test", Sink: sink})
|
||||
loaded, err := sessionstore.LoadSession(path)
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
return c, loaded, path
|
||||
}
|
||||
|
||||
// A fold saved beside a session that no longer matches the history behind it is
|
||||
// not reused, and the person is told: the whole history goes out instead.
|
||||
func TestResumeSaysWhenASavedFoldIsNotReused(t *testing.T) {
|
||||
var got []event.Event
|
||||
c, loaded, path := foldController(t, event.FuncSink(func(e event.Event) { got = append(got, e) }), "not-the-prefix-hash")
|
||||
defer c.Close()
|
||||
|
||||
c.Resume(loaded, path)
|
||||
|
||||
codes := foldNoticeCodes(got)
|
||||
if len(codes) != 1 || codes[0] != event.NoticeCodeFoldNotReusedLineage {
|
||||
t.Fatalf("fold notices = %v, want exactly the lineage one (the sidecar carries no lineage key)", codes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResumeSaysNothingWhenTheFoldIsReused(t *testing.T) {
|
||||
var got []event.Event
|
||||
msgs := []provider.Message{{Role: provider.RoleSystem, Content: "sys"}, {Role: provider.RoleUser, Content: "task"}}
|
||||
c, loaded, path := foldController(t, event.FuncSink(func(e event.Event) { got = append(got, e) }), sessionstore.CoveredPrefixHash(msgs, 2))
|
||||
defer c.Close()
|
||||
|
||||
c.Resume(loaded, path)
|
||||
|
||||
if codes := foldNoticeCodes(got); len(codes) != 0 {
|
||||
t.Fatalf("a reusable fold produced notices %v", codes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResumeWithoutASavedFoldSaysNothing(t *testing.T) {
|
||||
var got []event.Event
|
||||
c, path := coldNoticeController(t, event.FuncSink(func(e event.Event) { got = append(got, e) }))
|
||||
loaded, err := sessionstore.LoadSession(path)
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
c.Resume(loaded, path)
|
||||
|
||||
if codes := foldNoticeCodes(got); len(codes) != 0 {
|
||||
t.Fatalf("no fold on disk, yet notices %v", codes)
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,25 @@ func (c *Controller) bindExecutorProjection(path string, loadSidecar bool) {
|
||||
return
|
||||
}
|
||||
c.executor.BindSessionPath(path, loadSidecar)
|
||||
if loadSidecar {
|
||||
c.announceUnusedFold()
|
||||
}
|
||||
}
|
||||
|
||||
// announceUnusedFold says why a fold saved beside the session was not reused: the
|
||||
// full history goes to the model instead, and only the host knows which reason.
|
||||
func (c *Controller) announceUnusedFold() {
|
||||
if c.sink == nil {
|
||||
return
|
||||
}
|
||||
switch c.executor.ProjectionDropped() {
|
||||
case agent.ProjectionDropHistory:
|
||||
c.sink.Emit(event.Event{Kind: event.Notice, Level: event.LevelWarn, Code: event.NoticeCodeFoldNotReusedHistory,
|
||||
Text: "A saved compaction of this conversation was not reused: the history it summarised has changed since. The full history is sent instead."})
|
||||
case agent.ProjectionDropLineage:
|
||||
c.sink.Emit(event.Event{Kind: event.Notice, Level: event.LevelWarn, Code: event.NoticeCodeFoldNotReusedLineage,
|
||||
Text: "A saved compaction of this conversation was made under another model or workspace and does not match its history. The full history is sent instead."})
|
||||
}
|
||||
}
|
||||
|
||||
// maybeColdResumePrune records warm/cold/unknown only; it never rewrites
|
||||
|
||||
@@ -16,6 +16,7 @@ type ConflictError struct {
|
||||
SessionID string `json:"sessionId"`
|
||||
Paths []string `json:"paths"`
|
||||
RequestedPaths []string `json:"requestedPaths"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
Cause error `json:"-"`
|
||||
}
|
||||
|
||||
@@ -34,7 +35,7 @@ func (o *Owner) extent(paths []string) []string {
|
||||
}
|
||||
|
||||
func (o *Owner) conflict(holder, sessionID string, held, requested []string) *ConflictError {
|
||||
return &ConflictError{Holder: holder, SessionID: sessionID, Paths: o.extent(held), RequestedPaths: o.extent(requested), Cause: errHeld}
|
||||
return &ConflictError{Holder: holder, SessionID: sessionID, Paths: o.extent(held), RequestedPaths: o.extent(requested), Repository: o.scope.repository, Cause: errHeld}
|
||||
}
|
||||
|
||||
// SetSessionID binds the stable session identity independently of its name.
|
||||
|
||||
@@ -55,6 +55,9 @@ type Wait struct {
|
||||
HolderSessionID string
|
||||
Paths []string
|
||||
RequestedPaths []string
|
||||
// Repository is the git repository root that folded this session's folder
|
||||
// into the shared writer domain; empty when the domain is the folder itself.
|
||||
Repository string
|
||||
}
|
||||
|
||||
// WaitNotice receives both ends of a reported wait. It must return quickly and
|
||||
@@ -131,7 +134,7 @@ var localRegistry = struct {
|
||||
// shared by Reasonix processes for cross-process protection; it is kept outside
|
||||
// the workspace so acquiring a lease never dirties user files.
|
||||
func New(workspaceRoot, lockDir string, onWait WaitNotice, opts ...Option) (*Owner, error) {
|
||||
canonical, err := CanonicalWorkspace(workspaceRoot)
|
||||
selected, canonical, err := canonicalWorkspaceParts(workspaceRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -160,6 +163,9 @@ func New(workspaceRoot, lockDir string, onWait WaitNotice, opts ...Option) (*Own
|
||||
local: local,
|
||||
scope: pathLeaseState{root: canonical, identity: rand.Text()},
|
||||
}
|
||||
if selected != canonical {
|
||||
o.scope.repository = canonical
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
@@ -170,25 +176,34 @@ func New(workspaceRoot, lockDir string, onWait WaitNotice, opts ...Option) (*Own
|
||||
// resolves symlinks when possible and folds case on Windows, where paths are
|
||||
// case-insensitive by default.
|
||||
func CanonicalWorkspace(root string) (string, error) {
|
||||
_, domain, err := canonicalWorkspaceParts(root)
|
||||
return domain, err
|
||||
}
|
||||
|
||||
// canonicalWorkspaceParts returns the folder as selected and the writer domain it
|
||||
// belongs to; they differ exactly when a git repository above the folder folds it in.
|
||||
func canonicalWorkspaceParts(root string) (selected, domain string, err error) {
|
||||
root = strings.TrimSpace(root)
|
||||
if root == "" {
|
||||
return "", errors.New("workspace root is empty")
|
||||
return "", "", errors.New("workspace root is empty")
|
||||
}
|
||||
abs, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve workspace root: %w", err)
|
||||
return "", "", fmt.Errorf("resolve workspace root: %w", err)
|
||||
}
|
||||
abs = filepath.Clean(abs)
|
||||
if resolved, resolveErr := fileutil.ResolveExistingPath(abs); resolveErr == nil {
|
||||
abs = filepath.Clean(resolved)
|
||||
} else if !os.IsNotExist(resolveErr) {
|
||||
return "", fmt.Errorf("canonicalize workspace root: %w", resolveErr)
|
||||
return "", "", fmt.Errorf("canonicalize workspace root: %w", resolveErr)
|
||||
}
|
||||
abs = nearestGitWorktreeRoot(abs)
|
||||
if runtime.GOOS == "windows" {
|
||||
abs = strings.ToLower(filepath.ToSlash(abs))
|
||||
fold := func(p string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return strings.ToLower(filepath.ToSlash(p))
|
||||
}
|
||||
return p
|
||||
}
|
||||
return abs, nil
|
||||
return fold(abs), fold(nearestGitWorktreeRoot(abs)), nil
|
||||
}
|
||||
|
||||
// nearestGitWorktreeRoot folds a repository root and any selected directory
|
||||
@@ -421,7 +436,7 @@ func (w *waitClock) close(outcome WaitOutcome) {
|
||||
}
|
||||
|
||||
func (w *waitClock) notice(outcome WaitOutcome, elapsed time.Duration) Wait {
|
||||
n := Wait{Outcome: outcome, Elapsed: elapsed, Holder: w.holder, RequestedPaths: w.requested}
|
||||
n := Wait{Outcome: outcome, Elapsed: elapsed, Holder: w.holder, RequestedPaths: w.requested, Repository: w.owner.scope.repository}
|
||||
if c := w.conflict; c != nil {
|
||||
n.Holder, n.HolderSessionID, n.Paths = c.Holder, c.SessionID, c.Paths
|
||||
}
|
||||
|
||||
@@ -571,3 +571,86 @@ func TestACrossProcessWaitNamesTheHolder(t *testing.T) {
|
||||
t.Fatalf("wait reports = %+v, want the first one naming the other process's session", log.seen)
|
||||
}
|
||||
}
|
||||
|
||||
// Two folders that only share a git repository above them wait on each other,
|
||||
// and the report says the shared domain is that repository: a holder and a
|
||||
// requester showing one identical path otherwise reads as a session fighting itself.
|
||||
func TestAWaitNamesTheRepositoryThatFoldsTwoFoldersIntoOneDomain(t *testing.T) {
|
||||
withWaitGrace(t, 20*time.Millisecond)
|
||||
repo, locks := testenv.TempDir(t), testenv.TempDir(t)
|
||||
if err := os.Mkdir(filepath.Join(repo, ".git"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
projectA, projectB := filepath.Join(repo, "a"), filepath.Join(repo, "b")
|
||||
for _, dir := range []string{projectA, projectB} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
first, err := New(projectA, locks, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var log waitLog
|
||||
second, err := New(projectB, locks, log.note)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first.BeginRun()
|
||||
second.BeginRun()
|
||||
if err := first.AcquireWrite(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
acquired := make(chan error, 1)
|
||||
go func() { acquired <- second.AcquireWrite(context.Background()) }()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
first.EndRun()
|
||||
if err := <-acquired; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second.EndRun()
|
||||
want, err := CanonicalWorkspace(repo)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
log.mu.Lock()
|
||||
defer log.mu.Unlock()
|
||||
if len(log.seen) == 0 || log.seen[0].Repository != want {
|
||||
t.Fatalf("wait reports = %+v, want the first to name repository %q", log.seen, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWaitOnTheRepositoryRootItselfNamesNoRepository(t *testing.T) {
|
||||
withWaitGrace(t, 20*time.Millisecond)
|
||||
repo, locks := testenv.TempDir(t), testenv.TempDir(t)
|
||||
if err := os.Mkdir(filepath.Join(repo, ".git"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first, err := New(repo, locks, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var log waitLog
|
||||
second, err := New(repo, locks, log.note)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first.BeginRun()
|
||||
second.BeginRun()
|
||||
if err := first.AcquireWrite(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
acquired := make(chan error, 1)
|
||||
go func() { acquired <- second.AcquireWrite(context.Background()) }()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
first.EndRun()
|
||||
if err := <-acquired; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second.EndRun()
|
||||
log.mu.Lock()
|
||||
defer log.mu.Unlock()
|
||||
if len(log.seen) == 0 || log.seen[0].Repository != "" {
|
||||
t.Fatalf("wait reports = %+v, want no repository on the root's own wait", log.seen)
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ type pathLeaseState struct {
|
||||
record string
|
||||
sessionID func() string
|
||||
identity string
|
||||
// repository is the git root that folds the selected folder into this domain; empty when none does.
|
||||
repository string
|
||||
}
|
||||
|
||||
type pathLeaseRecord struct {
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
本版本新增会话概览、从源文件夹更新技能、更新提醒、按模型填写价格、子代理乐观写入范围和从分支创建独立工作区副本,并修复停止卡住、后台任务状态、同仓库项目互相等待写入、压缩恢复和一批界面问题。
|
||||
|
||||
## 新增
|
||||
|
||||
- 「我的反馈」新增「账号关联」:登录后可手动把这台电脑关联到账号,随时解除;默认关闭、不改变额度,网页账号页之后据此显示反馈等级。关联 #13141。
|
||||
- 会话右键菜单新增“从最近回复分叉”,会话还在执行时也能从最近一个已完成的回复分叉;旧回复下的分叉按钮执行中同样可用。关联 #13177。
|
||||
- 新增:侧边栏会话右键菜单加入“自动命名”,一步生成并保存标题,无需先打开重命名,关联 #13195
|
||||
- 新增:设置 › 会话折叠 › 思考过程新增「收起并预览首句」,收起的思考在同一行后面显示首句(超长以 … 截断)。默认不变。关联 #13212。
|
||||
- 新增:网页版 Studio 页面开着不关、内核升级后,会提示“这个页面不是最新版”并给出刷新按钮,不再出现 n.filter 之类的报错,关联 #13213
|
||||
- 新增:删除带有恢复副本的会话时,确认框可以选择“连同 N 份副本删除”,不用再一个一个删,关联 #13221
|
||||
- 新增:文件窗口里可以用“用其他程序打开项目”菜单,在 VS Code、终端等已安装的程序中打开当前项目,关联 #13227
|
||||
- 新增:内置浏览器的页签可以双击重命名,不再只能显示网址,关联 #13228
|
||||
- 新增:Windows 上可以给「指定一个可执行文件」填写启动参数(例如 wsl.exe -d 发行版 -- /bin/bash),保存前带着参数实际运行验证,无法运行则不保存。关联 #13247。
|
||||
- 新增:右侧文件窗口的工具栏加了“在系统文件管理器中显示此文件”按钮,不用再去文件列表里右键,关联 #13255
|
||||
- 新增:回复反馈时可以附上截图(粘贴或选择,最多 3 张),只发给维护者,由维护者决定是否公开。关联 #13258。
|
||||
- 新增:可以从选定的分支创建独立的工作区副本,指定的分支不存在时会明确提示。关联 #13271。
|
||||
- 新增:设置里「子代理的写入范围」可开启乐观模式(默认关闭):没有声明 write_paths 的子代理不再占住整个工作区,可与主会话并发写。关联 #13274。
|
||||
- 新增:连接编辑器里可以按模型填写每百万 token 的价格(输入、输出、缓存命中、货币),没价格的模型在用量里仍显示「未知」。关联 #13276。
|
||||
- 新增:设置 → 版本里的“更新方式”。默认“手动”不做后台联网;选“提醒”后每天查一次版本目录,有新版本时设置图标出现小点。两种方式都不下载、不安装、不重启。关联 #13291。
|
||||
- 新增:点击输入框下的“缓存”数字打开会话概览:请求、Token、缓存命中、工作时长、费用及按来源明细,各数字旁写明口径,费用不完整时只给下限。关联 #13297。
|
||||
- 新增:会话概览里加上“上下文窗口”:已使用、容量、各类占比与自动压缩线,标明是下一次请求的当前值而非累计,可直达上下文详情。关联 #13297。
|
||||
- 新增:技能列表会标出从某个文件夹安装的技能副本与来源文件夹是否一致(来源有更新、副本被改过、两边都改过、来源不见了)。关联 #13311。
|
||||
- 新增:技能列表里,从本机文件夹复制安装的技能在来源有变化时可以“从源文件夹更新…”:先预览每个文件的改动(脚本需逐项展开),确认后才替换,失败会保留原副本。关联 #13311。
|
||||
|
||||
## 变更
|
||||
|
||||
- 侧栏底部的「发送反馈」现在用强调色加粗显示,更容易找到(不加动画;错误提示上的「报告此问题」和命令面板入口不变)。关联 #13048。
|
||||
- MCP 服务器把耗时的工具调用作为任务返回时,现在会自动跟进到结果,不再返回空结果或要求任务扩展而报错。关联 #13108。
|
||||
- MCP 任务超过工具超时后,会转成后台任务继续运行,可用 wait、bash_output 取结果、kill_shell 取消,不再直接超时失败。关联 #13108。
|
||||
- 技能列表里,技能声明的许可(license)、运行要求(compatibility)和附加信息(metadata)现在可以展开查看;这些只是显示,不影响技能怎么运行。关联 #13110。
|
||||
- 会话很多时侧栏刷新、改名更快:每次刷新不再逐个会话解析路径(Windows 上 2100 个会话约从 1.2 秒降到 0.4 秒)。关联 #13176。
|
||||
- 用量页的「每日明细」现在最新一天排在最上面,不用再滚到底才能看到最近的数据。关联 #13179。
|
||||
- 打开文件失败时按真实原因提示:只有文件确实不存在才说“找不到文件”;被软链接或权限拒绝的文件不再被误报为不存在。关联 #13180。
|
||||
- 输入 / 弹出命令列表更快:补全命令名时不再每次读取插件包、模型和记忆数据,装了大量插件技能时尤其明显。关联 #13197。
|
||||
- 输入 / 或 @ 时,上一个按键还没返回的补全请求会被取消,不再堆积占满连接。关联 #13197。
|
||||
- 设置了提问超时后,提问卡片会显示“N 秒后自动跳过,点任意处保留”的倒计时,接管后消失;命令行界面同步显示。关联 #13210。
|
||||
- 分支芯片区分“不是 Git 仓库”“Git 拒绝打开”“Git 没有及时响应”,并显示 Git 的原话,不再统一说不是仓库。关联 #13217。
|
||||
- 启动时 Git 没及时响应不再让分支芯片整场会话都显示没有仓库:第一次读取时会再试一次。关联 #13217。
|
||||
- 改进:配置文件无法读取导致设置没保存时,提示会指到「设置」里查看出错位置并修复,关联 #13220
|
||||
- 优化:切换本地 Git 分支成功后立即使用切换结果更新界面,变更列表单独刷新,状态摘要的三项读取并行进行。关联 #12780。
|
||||
- 改进:会话标签页里正在工作的会话状态点会呼吸闪动,已完成未读也直接由这个点表示(不再多出一个小点),关联 #13254
|
||||
- 改进:带 write_paths 的子代理调用不允许的工具时,拒绝原因现在说明缘由和下一步(不要重试,改用文件工具或收尾汇报)。关联 #13269。
|
||||
- 改进:压缩完成的卡片在折叠后仍占窗口很大一块时,会说明剩下的是什么(模型回复、工具输出、用户消息各多少,最大的一条是谁)。关联 #13284。
|
||||
- 改进:更新下载卡住时,版本面板会说明“已重连 N 次”;重连仍无效则提示下载卡住,并给出重试或手动下载的办法。关联 #13291。
|
||||
- 改变:输入框下的缓存百分比不再把服务商未区分缓存的输入算作未命中,与会话概览同一口径;这类输入多的会话读数更高。关联 #13297。
|
||||
- 改进:对话右下角“回到顶部/底部”的按钮更淡更小,改为圆角方形、半透明浅底,不再是深色圆形。关联 #13303。
|
||||
- 改进:给“额外可写文件夹”选择技能安装位置时,拒绝原因现在说明出路:把技能源文件夹用“登记”方式安装,并在源文件夹里修改。关联 #13311。
|
||||
|
||||
## 修复
|
||||
|
||||
- 修复超长会话里自动压缩反复放弃的问题:摘要模型没看到的改动不再被当成「摘要丢了全部改动」,由系统补记后正常折叠。关联 #13013。
|
||||
- 修复:重命名或复制的文件在 Git 变更里的新增/删除行数现在能正确统计。关联 #13175。
|
||||
- 修复鼠标移向状态栏里的上下文浮窗时它立刻消失、导致点不到「立即压缩」和「管理 MCP」的问题(浮窗与触发处之间的空隙现在会保持悬停)。关联 #13178。
|
||||
- 修复:设置了壁纸时,标签栏下沿不再是一条生硬的横线,而是向下柔和渐隐,关联 #13194
|
||||
- 修复:已过期的置顶记忆不再进入稳定前缀、不再遮蔽全局指引、也不再占用置顶字数预算。关联 #13201。
|
||||
- 修复:关闭一个内核里已不存在的会话窗格时不再弹出 “no runtime r1”,按已关闭处理,关联 #13204
|
||||
- 修复:电脑崩溃后会话检查点的最后一行写了一半时,不再丢掉其余完好的对话轮次。关联 #13241。
|
||||
- 修复对话栏变窄时输入框工具栏(模型、思考强度、停下按钮)折成两行错位的问题:现在始终保持一行,放不下时模型名先缩略。关联 #13259。
|
||||
- 修复:设置了独立的 REASONIX_HOME 时,配置编辑锁不再写到系统默认的 ~/.reasonix,保持独立目录自包含。关联 #13275。
|
||||
- 修复:窗口截图的对话框已经关闭时,截图流程会安静停止,不再报“window is not defined”类错误。关联 #13285。
|
||||
- 修复:「恢复旧会话」现在能导入 1.x 桌面版新存储(desktop-sessions-v5)里的会话,此前它们被悄悄跳过;无法使用的编号会列入“未导入”。关联 #13290。
|
||||
- 修复:项目菜单里的“文件管理器”“终端”现在显示中文,且“用文件管理器打开”不再与“在文件管理器中显示”重复。关联 #13292。
|
||||
- 修复:改动范围无法确认时,结束前的提示会说明检查解决不了它,要换用会报告路径的工具重做。关联 #13293。
|
||||
- 修复:打开会话时,之前保存的压缩结果若因历史变化或模型/工作区不同而无法复用,现在会提示原因,不再悄悄改发完整历史。关联 #13295。
|
||||
- 修复:Windows 上停止任务时,如果系统的 taskkill 卡住,整个“停止”不再被它无限期拖住:最多等 10 秒,之后直接结束主进程。关联 #13299。
|
||||
- 修复:顶部的计划按钮不再停在标签行中间,而是靠右紧挨“辅助会话”。关联 #13302。
|
||||
- 修复:停止后把同一句话再发一次,不再在待发队列里叠出多份;原来未生效的那条被重新排队,恢复后只会回答一次。关联 #13318。
|
||||
- 修复:停止后台任务后,右下角的任务列表会立即刷新;对已在停止中的任务再点停止,会说明“已请求停止,进程还没退出”,不再误报“已经不在运行”。关联 #13319。
|
||||
- 修复:从源文件夹更新技能后,记录写入失败会自动重试一次;仍失败时再点一次更新即可补上记录,技能不再被误判为“副本被改过”。关联 #13311。
|
||||
- 修复:对话右下角“回到会话顶部/跳到最新”按钮一次点击直达两端;原来的“逐个展开块”跳法改为按住 Shift 点击。关联 #13326、#13229。
|
||||
- 修复:会话概览里同一行的数字按同一高度对齐,不再因旁边格的脚注较长而下沉。关联 #13297。
|
||||
- 修复:同一个 Git 仓库下的多个项目互相等待写入时,提示现在说明它们共用该仓库这一个写入域,并给出出路。关联 #13335。
|
||||
|
||||
---
|
||||
|
||||
<!-- lang:en -->
|
||||
|
||||
This release adds a session overview, updating skills from their source folder, update notifications, per-model prices and an optimistic sub-agent write mode, and fixes stuck Stop, background-job state, projects in one repository waiting on each other and compaction recovery.
|
||||
|
||||
## New
|
||||
|
||||
- "My feedback" gains an "Account link": once signed in, link this computer yourself and unlink any time. Off by default, changes no limits. Refs #13141.
|
||||
- New "Branch from the latest reply" in the session menu: a running conversation can branch from its latest finished reply, and older replies' branch buttons work mid-run. Refs #13177.
|
||||
- Added: the sidebar session menu now has Auto name, which generates and saves a title in one step without opening Rename first, refs #13195
|
||||
- Added: Settings › Conversation folding › Thinking gains "Folded, with first sentence", showing the first sentence after a folded thought (long ones end in …). Default unchanged. Refs #13212.
|
||||
- Added: a hosted Studio page left open across an upgrade now says it is not the latest build and offers a refresh, instead of failing with errors like n.filter, refs #13213
|
||||
- Added: deleting a conversation that has recovery copies now offers to delete the copies with it, instead of one by one, refs #13221
|
||||
- Added: the file window has an "open the project in another program" menu (VS Code, terminal and the other programs installed here), refs #13227
|
||||
- Added: built-in browser tabs can be renamed by double-clicking, instead of always showing the address, refs #13228
|
||||
- Added: on Windows, "point at an executable" takes launcher arguments (for example wsl.exe -d <distro> -- /bin/bash), verified by running them before saving. Refs #13247.
|
||||
- Added: the file window in the right sidebar has a "show this file in the system file manager" button in its toolbar, so you no longer need the file list right-click, refs #13255
|
||||
- Added: replies to feedback can carry screenshots (paste or pick, up to 3); they go to the maintainers only, who decide whether to make them public. Refs #13258.
|
||||
- Added: an isolated workspace copy can be created from a chosen branch, and an unknown branch is reported clearly. Refs #13271.
|
||||
- Added: an optimistic mode for sub-agent write scope in Settings (off by default): a sub-agent with no write_paths no longer holds the whole workspace. Refs #13274.
|
||||
- Added: the connection editor takes a per-model price (input, output, cache hit, currency); a model with no price still shows as unknown in usage. Refs #13276.
|
||||
- Added: Settings, Version, "Update mode". Manual (default) does nothing in the background; Notify checks daily and dots the Settings icon. Neither downloads, installs or restarts. Refs #13291.
|
||||
- Added: the cache figure under the composer opens a session overview: requests, tokens, cache hit, work time, cost, by source, each with its definition; incomplete cost shows a floor. Refs #13297.
|
||||
- Added: the session overview shows the context window: used, capacity, share by kind and the fold line, stated as a current reading, with a way into the context details. Refs #13297.
|
||||
- Added: the Skills list marks an installed skill copy that no longer matches the folder it came from: source changed, copy edited, both changed, or source gone. Refs #13311.
|
||||
- Added: a skill copied from a local folder can be updated from it in the Skills list: preview each file change (scripts must be opened), confirm; a failure keeps the old copy. Refs #13311.
|
||||
|
||||
## Changed
|
||||
|
||||
- The sidebar footer's "Send feedback" is now bold in the accent colour so it is easier to find (no animation; other feedback entries unchanged). Refs #13048.
|
||||
- MCP servers that answer a long tool call as a task are now followed to the result, instead of returning nothing or failing for lack of the tasks extension. Refs #13108.
|
||||
- An MCP task that outruns the tool's timeout now continues as a background job you can wait on, read with bash_output or cancel with kill_shell, instead of failing on the timeout. Refs #13108.
|
||||
- A skill's declared license, requirements (compatibility) and metadata can now be opened in the skills list; display only, nothing about how it runs changes. Refs #13110.
|
||||
- With many sessions the sidebar refreshes and renames faster: a refresh no longer resolves every session's path (2100 sessions on Windows: about 1.2 s to 0.4 s). Refs #13176.
|
||||
- The usage page's daily breakdown now lists the newest day first, so the latest figures are visible without scrolling to the bottom. Refs #13179.
|
||||
- Opening a file that fails now says why: only a file that is not there is reported as missing, and one behind a refused link or a permission error is no longer called "not found". Refs #13180.
|
||||
- Typing / opens the command list faster: completing a command name no longer reads plugin packages, models and memory on every keystroke, which matters most with many plugin skills. Refs #13197.
|
||||
- While typing / or @, a completion request a newer keystroke has made stale is cancelled instead of piling up on the connection pool. Refs #13197.
|
||||
- With an ask timeout set, the question card now counts down "skipped in N s, click anywhere to keep it" and stops once you take the question over; the terminal UI shows it too. Refs #13210.
|
||||
- The branch chip tells "not a Git repository" from "Git refused to open it" and "Git did not respond", and shows Git's own words. Refs #13217.
|
||||
- If Git does not answer when a session opens, the branch chip no longer says there is no repository for the whole session: the first read tries once more. Refs #13217.
|
||||
- Improved: when a setting is not saved because the config file does not parse, the message now points to Settings, where the broken line is shown and can be repaired, refs #13220
|
||||
- Improved: after a successful local Git branch switch, the UI uses the returned state at once and refreshes the change list on its own; the summary's three reads now overlap. Refs #12780.
|
||||
- Improved: the session tab dot breathes while a session is working, and finished-but-unread is shown by that same dot (no extra small dot), refs #13254
|
||||
- Improved: when a write_paths sub-agent calls a target it may not run, the refusal now says why and what to do next (do not retry; use file tools or report). Refs #13269.
|
||||
- Improved: when a fold still leaves a large share of the window, its card says what the rest is made of and which single message is the largest. Refs #13284.
|
||||
- Improved: when an update download goes quiet, the version panel says it reconnected N times, and if that fails says it got stuck, with a retry or manual installer. Refs #13291.
|
||||
- Changed: the cache percentage under the composer no longer counts prompt the provider did not split as uncached, matching the overview; sessions with much of it read higher. Refs #13297.
|
||||
- Improved: the jump-to-top/bottom buttons are lighter and smaller, rounded squares with a translucent pale fill instead of dark circles. Refs #13303.
|
||||
- Improved: granting the installed-skills folder as a writable folder now explains the way forward: install the skill's source folder in register mode and edit it there. Refs #13311.
|
||||
|
||||
## Fixed
|
||||
|
||||
- Fixed auto-compaction repeatedly giving up in very long sessions: changes the summarizer never saw are no longer treated as lost by it. Refs #13013.
|
||||
- Fixed: added and removed line counts are now correct for renamed and copied files in the Git changes. Refs #13175.
|
||||
- Fixed the status-bar context card closing as the pointer moved toward it, which made "Compress now" and "Manage MCP" unreachable; the gap now keeps the hover. Refs #13178.
|
||||
- Fixed: over a wallpaper the tab strip no longer ends on a hard line; it now fades softly into the conversation, refs #13194
|
||||
- Fixed: an expired pinned memory no longer rides the stable prefix, shadows live global guidance, or consumes the pinned-character budget. Refs #13201.
|
||||
- Fixed: closing a pane the kernel no longer holds no longer surfaces a raw "no runtime r1" error; it counts as closed, refs #13204
|
||||
- Fixed: when a crash leaves the last line of a session checkpoint half-written, the intact turns before it are no longer lost. Refs #13241.
|
||||
- Fixed the composer toolbar breaking into two misaligned lines when the conversation column is narrow: it stays on one line and the model name shortens first. Refs #13259.
|
||||
- Fixed: with an isolated REASONIX_HOME, config edit locks no longer go to the default ~/.reasonix; the isolated home stays self-contained. Refs #13275.
|
||||
- Fixed: the window capture now stops quietly when its dialog is already gone, instead of raising a "window is not defined" error. Refs #13285.
|
||||
- Fixed: "Recover older sessions" now imports 1.x desktop (desktop-sessions-v5) conversations, which were skipped silently; an unusable id is listed as skipped. Refs #13290.
|
||||
- Fixed: the project menu now shows "File manager" and "Terminal" in your language, and no longer lists the file manager twice. Refs #13292.
|
||||
- Fixed: when a change's extent is unknown, the end-of-turn prompt now says a check cannot settle it and to redo it with a path-reporting tool. Refs #13293.
|
||||
- Fixed: when a saved compaction cannot be reused on opening a session, a notice now says why instead of silently sending the full history. Refs #13295.
|
||||
- Fixed: on Windows a hung taskkill no longer holds a Stop forever; it is given 10 seconds, after which the main process is ended directly. Refs #13299.
|
||||
- Fixed: the plan button in the tab row now sits at the right edge beside the side-question toggle instead of floating mid-row. Fixes #13302.
|
||||
- Fixed: after a stop, sending the same words again no longer stacks copies in the held queue; the steer that did not apply is requeued, so once released it is answered once. Refs #13318.
|
||||
- Fixed: after stopping a background job the task list refreshes at once, and stopping one already shutting down says so instead of claiming it is not running. Refs #13319.
|
||||
- Fixed: a failed record write after a skill update is retried once; if it still fails, updating again repairs the record instead of leaving the skill marked edited. Refs #13311.
|
||||
- Fixed: the jump-to-top and jump-to-latest buttons now go straight to the ends in one click; the old step through expanded blocks is Shift+click. Refs #13326, #13229.
|
||||
- Fixed: in the session overview, figures on one row now share a baseline instead of sinking when the neighbouring cell's footnote is longer. Refs #13297.
|
||||
- Fixed: when projects under one Git repository wait on each other to write, the notice now says they share that repository's single write domain, and how to avoid it. Refs #13335.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复超长会话里自动压缩反复放弃的问题:摘要模型没看到的改动不再被当成「摘要丢了全部改动」,由系统补记后正常折叠。关联 #13013。
|
||||
- Fixed auto-compaction repeatedly giving up in very long sessions: changes the summarizer never saw are no longer treated as lost by it. Refs #13013.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 侧栏底部的「发送反馈」现在用强调色加粗显示,更容易找到(不加动画;错误提示上的「报告此问题」和命令面板入口不变)。关联 #13048。
|
||||
- The sidebar footer's "Send feedback" is now bold in the accent colour so it is easier to find (no animation; other feedback entries unchanged). Refs #13048.
|
||||
@@ -1,2 +0,0 @@
|
||||
- MCP 服务器把耗时的工具调用作为任务返回时,现在会自动跟进到结果,不再返回空结果或要求任务扩展而报错。关联 #13108。
|
||||
- MCP servers that answer a long tool call as a task are now followed to the result, instead of returning nothing or failing for lack of the tasks extension. Refs #13108.
|
||||
@@ -1,2 +0,0 @@
|
||||
- MCP 任务超过工具超时后,会转成后台任务继续运行,可用 wait、bash_output 取结果、kill_shell 取消,不再直接超时失败。关联 #13108。
|
||||
- An MCP task that outruns the tool's timeout now continues as a background job you can wait on, read with bash_output or cancel with kill_shell, instead of failing on the timeout. Refs #13108.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 技能列表里,技能声明的许可(license)、运行要求(compatibility)和附加信息(metadata)现在可以展开查看;这些只是显示,不影响技能怎么运行。关联 #13110。
|
||||
- A skill's declared license, requirements (compatibility) and metadata can now be opened in the skills list; display only, nothing about how it runs changes. Refs #13110.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 「我的反馈」新增「账号关联」:登录后可手动把这台电脑关联到账号,随时解除;默认关闭、不改变额度,网页账号页之后据此显示反馈等级。关联 #13141。
|
||||
- "My feedback" gains an "Account link": once signed in, link this computer yourself and unlink any time. Off by default, changes no limits. Refs #13141.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:重命名或复制的文件在 Git 变更里的新增/删除行数现在能正确统计。关联 #13175。
|
||||
- Fixed: added and removed line counts are now correct for renamed and copied files in the Git changes. Refs #13175.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 会话很多时侧栏刷新、改名更快:每次刷新不再逐个会话解析路径(Windows 上 2100 个会话约从 1.2 秒降到 0.4 秒)。关联 #13176。
|
||||
- With many sessions the sidebar refreshes and renames faster: a refresh no longer resolves every session's path (2100 sessions on Windows: about 1.2 s to 0.4 s). Refs #13176.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 会话右键菜单新增“从最近回复分叉”,会话还在执行时也能从最近一个已完成的回复分叉;旧回复下的分叉按钮执行中同样可用。关联 #13177。
|
||||
- New "Branch from the latest reply" in the session menu: a running conversation can branch from its latest finished reply, and older replies' branch buttons work mid-run. Refs #13177.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复鼠标移向状态栏里的上下文浮窗时它立刻消失、导致点不到「立即压缩」和「管理 MCP」的问题(浮窗与触发处之间的空隙现在会保持悬停)。关联 #13178。
|
||||
- Fixed the status-bar context card closing as the pointer moved toward it, which made "Compress now" and "Manage MCP" unreachable; the gap now keeps the hover. Refs #13178.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 用量页的「每日明细」现在最新一天排在最上面,不用再滚到底才能看到最近的数据。关联 #13179。
|
||||
- The usage page's daily breakdown now lists the newest day first, so the latest figures are visible without scrolling to the bottom. Refs #13179.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 打开文件失败时按真实原因提示:只有文件确实不存在才说“找不到文件”;被软链接或权限拒绝的文件不再被误报为不存在。关联 #13180。
|
||||
- Opening a file that fails now says why: only a file that is not there is reported as missing, and one behind a refused link or a permission error is no longer called "not found". Refs #13180.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:设置了壁纸时,标签栏下沿不再是一条生硬的横线,而是向下柔和渐隐,关联 #13194
|
||||
- Fixed: over a wallpaper the tab strip no longer ends on a hard line; it now fades softly into the conversation, refs #13194
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:侧边栏会话右键菜单加入“自动命名”,一步生成并保存标题,无需先打开重命名,关联 #13195
|
||||
- Added: the sidebar session menu now has Auto name, which generates and saves a title in one step without opening Rename first, refs #13195
|
||||
@@ -1,2 +0,0 @@
|
||||
- 输入 / 弹出命令列表更快:补全命令名时不再每次读取插件包、模型和记忆数据,装了大量插件技能时尤其明显。关联 #13197。
|
||||
- Typing / opens the command list faster: completing a command name no longer reads plugin packages, models and memory on every keystroke, which matters most with many plugin skills. Refs #13197.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 输入 / 或 @ 时,上一个按键还没返回的补全请求会被取消,不再堆积占满连接。关联 #13197。
|
||||
- While typing / or @, a completion request a newer keystroke has made stale is cancelled instead of piling up on the connection pool. Refs #13197.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:关闭一个内核里已不存在的会话窗格时不再弹出 “no runtime r1”,按已关闭处理,关联 #13204
|
||||
- Fixed: closing a pane the kernel no longer holds no longer surfaces a raw "no runtime r1" error; it counts as closed, refs #13204
|
||||
@@ -1,2 +0,0 @@
|
||||
- 设置了提问超时后,提问卡片会显示“N 秒后自动跳过,点任意处保留”的倒计时,接管后消失;命令行界面同步显示。关联 #13210。
|
||||
- With an ask timeout set, the question card now counts down "skipped in N s, click anywhere to keep it" and stops once you take the question over; the terminal UI shows it too. Refs #13210.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:设置 › 会话折叠 › 思考过程新增「收起并预览首句」,收起的思考在同一行后面显示首句(超长以 … 截断)。默认不变。关联 #13212。
|
||||
- Added: Settings › Conversation folding › Thinking gains "Folded, with first sentence", showing the first sentence after a folded thought (long ones end in …). Default unchanged. Refs #13212.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:网页版 Studio 页面开着不关、内核升级后,会提示“这个页面不是最新版”并给出刷新按钮,不再出现 n.filter 之类的报错,关联 #13213
|
||||
- Added: a hosted Studio page left open across an upgrade now says it is not the latest build and offers a refresh, instead of failing with errors like n.filter, refs #13213
|
||||
@@ -1,2 +0,0 @@
|
||||
- 分支芯片区分“不是 Git 仓库”“Git 拒绝打开”“Git 没有及时响应”,并显示 Git 的原话,不再统一说不是仓库。关联 #13217。
|
||||
- The branch chip tells "not a Git repository" from "Git refused to open it" and "Git did not respond", and shows Git's own words. Refs #13217.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 启动时 Git 没及时响应不再让分支芯片整场会话都显示没有仓库:第一次读取时会再试一次。关联 #13217。
|
||||
- If Git does not answer when a session opens, the branch chip no longer says there is no repository for the whole session: the first read tries once more. Refs #13217.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 改进:配置文件无法读取导致设置没保存时,提示会指到「设置」里查看出错位置并修复,关联 #13220
|
||||
- Improved: when a setting is not saved because the config file does not parse, the message now points to Settings, where the broken line is shown and can be repaired, refs #13220
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:删除带有恢复副本的会话时,确认框可以选择“连同 N 份副本删除”,不用再一个一个删,关联 #13221
|
||||
- Added: deleting a conversation that has recovery copies now offers to delete the copies with it, instead of one by one, refs #13221
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:文件窗口里可以用“用其他程序打开项目”菜单,在 VS Code、终端等已安装的程序中打开当前项目,关联 #13227
|
||||
- Added: the file window has an "open the project in another program" menu (VS Code, terminal and the other programs installed here), refs #13227
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:内置浏览器的页签可以双击重命名,不再只能显示网址,关联 #13228
|
||||
- Added: built-in browser tabs can be renamed by double-clicking, instead of always showing the address, refs #13228
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:电脑崩溃后会话检查点的最后一行写了一半时,不再丢掉其余完好的对话轮次。关联 #13241。
|
||||
- Fixed: when a crash leaves the last line of a session checkpoint half-written, the intact turns before it are no longer lost. Refs #13241.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:Windows 上可以给「指定一个可执行文件」填写启动参数(例如 wsl.exe -d 发行版 -- /bin/bash),保存前带着参数实际运行验证,无法运行则不保存。关联 #13247。
|
||||
- Added: on Windows, "point at an executable" takes launcher arguments (for example wsl.exe -d <distro> -- /bin/bash), verified by running them before saving. Refs #13247.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 改进:会话标签页里正在工作的会话状态点会呼吸闪动,已完成未读也直接由这个点表示(不再多出一个小点),关联 #13254
|
||||
- Improved: the session tab dot breathes while a session is working, and finished-but-unread is shown by that same dot (no extra small dot), refs #13254
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:右侧文件窗口的工具栏加了“在系统文件管理器中显示此文件”按钮,不用再去文件列表里右键,关联 #13255
|
||||
- Added: the file window in the right sidebar has a "show this file in the system file manager" button in its toolbar, so you no longer need the file list right-click, refs #13255
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:回复反馈时可以附上截图(粘贴或选择,最多 3 张),只发给维护者,由维护者决定是否公开。关联 #13258。
|
||||
- Added: replies to feedback can carry screenshots (paste or pick, up to 3); they go to the maintainers only, who decide whether to make them public. Refs #13258.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复对话栏变窄时输入框工具栏(模型、思考强度、停下按钮)折成两行错位的问题:现在始终保持一行,放不下时模型名先缩略。关联 #13259。
|
||||
- Fixed the composer toolbar breaking into two misaligned lines when the conversation column is narrow: it stays on one line and the model name shortens first. Refs #13259.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 改进:带 write_paths 的子代理调用不允许的工具时,拒绝原因现在说明缘由和下一步(不要重试,改用文件工具或收尾汇报)。关联 #13269。
|
||||
- Improved: when a write_paths sub-agent calls a target it may not run, the refusal now says why and what to do next (do not retry; use file tools or report). Refs #13269.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:可以从选定的分支创建独立的工作区副本,指定的分支不存在时会明确提示。关联 #13271。
|
||||
- Added: an isolated workspace copy can be created from a chosen branch, and an unknown branch is reported clearly. Refs #13271.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:设置里「子代理的写入范围」可开启乐观模式(默认关闭):没有声明 write_paths 的子代理不再占住整个工作区,可与主会话并发写。关联 #13274。
|
||||
- Added: an optimistic mode for sub-agent write scope in Settings (off by default): a sub-agent with no write_paths no longer holds the whole workspace. Refs #13274.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:设置了独立的 REASONIX_HOME 时,配置编辑锁不再写到系统默认的 ~/.reasonix,保持独立目录自包含。关联 #13275。
|
||||
- Fixed: with an isolated REASONIX_HOME, config edit locks no longer go to the default ~/.reasonix; the isolated home stays self-contained. Refs #13275.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:连接编辑器里可以按模型填写每百万 token 的价格(输入、输出、缓存命中、货币),没价格的模型在用量里仍显示「未知」。关联 #13276。
|
||||
- Added: the connection editor takes a per-model price (input, output, cache hit, currency); a model with no price still shows as unknown in usage. Refs #13276.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 改进:压缩完成的卡片在折叠后仍占窗口很大一块时,会说明剩下的是什么(模型回复、工具输出、用户消息各多少,最大的一条是谁)。关联 #13284。
|
||||
- Improved: when a fold still leaves a large share of the window, its card says what the rest is made of and which single message is the largest. Refs #13284.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:窗口截图的对话框已经关闭时,截图流程会安静停止,不再报“window is not defined”类错误。关联 #13285。
|
||||
- Fixed: the window capture now stops quietly when its dialog is already gone, instead of raising a "window is not defined" error. Refs #13285.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:「恢复旧会话」现在能导入 1.x 桌面版新存储(desktop-sessions-v5)里的会话,此前它们被悄悄跳过;无法使用的编号会列入“未导入”。关联 #13290。
|
||||
- Fixed: "Recover older sessions" now imports 1.x desktop (desktop-sessions-v5) conversations, which were skipped silently; an unusable id is listed as skipped. Refs #13290.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 改进:更新下载卡住时,版本面板会说明“已重连 N 次”;重连仍无效则提示下载卡住,并给出重试或手动下载的办法。关联 #13291。
|
||||
- Improved: when an update download goes quiet, the version panel says it reconnected N times, and if that fails says it got stuck, with a retry or manual installer. Refs #13291.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:设置 → 版本里的“更新方式”。默认“手动”不做后台联网;选“提醒”后每天查一次版本目录,有新版本时设置图标出现小点。两种方式都不下载、不安装、不重启。关联 #13291。
|
||||
- Added: Settings, Version, "Update mode". Manual (default) does nothing in the background; Notify checks daily and dots the Settings icon. Neither downloads, installs or restarts. Refs #13291.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:项目菜单里的“文件管理器”“终端”现在显示中文,且“用文件管理器打开”不再与“在文件管理器中显示”重复。关联 #13292。
|
||||
- Fixed: the project menu now shows "File manager" and "Terminal" in your language, and no longer lists the file manager twice. Refs #13292.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:改动范围无法确认时,结束前的提示会说明检查解决不了它,要换用会报告路径的工具重做。关联 #13293。
|
||||
- Fixed: when a change's extent is unknown, the end-of-turn prompt now says a check cannot settle it and to redo it with a path-reporting tool. Refs #13293.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:点击输入框下的“缓存”数字打开会话概览:请求、Token、缓存命中、工作时长、费用及按来源明细,各数字旁写明口径,费用不完整时只给下限。关联 #13297。
|
||||
- Added: the cache figure under the composer opens a session overview: requests, tokens, cache hit, work time, cost, by source, each with its definition; incomplete cost shows a floor. Refs #13297.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 改变:输入框下的缓存百分比不再把服务商未区分缓存的输入算作未命中,与会话概览同一口径;这类输入多的会话读数更高。关联 #13297。
|
||||
- Changed: the cache percentage under the composer no longer counts prompt the provider did not split as uncached, matching the overview; sessions with much of it read higher. Refs #13297.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:Windows 上停止任务时,如果系统的 taskkill 卡住,整个“停止”不再被它无限期拖住:最多等 10 秒,之后直接结束主进程。关联 #13299。
|
||||
- Fixed: on Windows a hung taskkill no longer holds a Stop forever; it is given 10 seconds, after which the main process is ended directly. Refs #13299.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:顶部的计划按钮不再停在标签行中间,而是靠右紧挨“辅助会话”。关联 #13302。
|
||||
- Fixed: the plan button in the tab row now sits at the right edge beside the side-question toggle instead of floating mid-row. Fixes #13302.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 改进:对话右下角“回到顶部/底部”的按钮更淡更小,改为圆角方形、半透明浅底,不再是深色圆形。关联 #13303。
|
||||
- Improved: the jump-to-top/bottom buttons are lighter and smaller, rounded squares with a translucent pale fill instead of dark circles. Refs #13303.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 改进:给“额外可写文件夹”选择技能安装位置时,拒绝原因现在说明出路:把技能源文件夹用“登记”方式安装,并在源文件夹里修改。关联 #13311。
|
||||
- Improved: granting the installed-skills folder as a writable folder now explains the way forward: install the skill's source folder in register mode and edit it there. Refs #13311.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:技能列表会标出从某个文件夹安装的技能副本与来源文件夹是否一致(来源有更新、副本被改过、两边都改过、来源不见了)。关联 #13311。
|
||||
- Added: the Skills list marks an installed skill copy that no longer matches the folder it came from: source changed, copy edited, both changed, or source gone. Refs #13311.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 新增:技能列表里,从本机文件夹复制安装的技能在来源有变化时可以“从源文件夹更新…”:先预览每个文件的改动(脚本需逐项展开),确认后才替换,失败会保留原副本。关联 #13311。
|
||||
- Added: a skill copied from a local folder can be updated from it in the Skills list: preview each file change (scripts must be opened), confirm; a failure keeps the old copy. Refs #13311.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:停止后把同一句话再发一次,不再在待发队列里叠出多份;原来未生效的那条被重新排队,恢复后只会回答一次。关联 #13318。
|
||||
- Fixed: after a stop, sending the same words again no longer stacks copies in the held queue; the steer that did not apply is requeued, so once released it is answered once. Refs #13318.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:停止后台任务后,右下角的任务列表会立即刷新;对已在停止中的任务再点停止,会说明“已请求停止,进程还没退出”,不再误报“已经不在运行”。关联 #13319。
|
||||
- Fixed: after stopping a background job the task list refreshes at once, and stopping one already shutting down says so instead of claiming it is not running. Refs #13319.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:从源文件夹更新技能后,记录写入失败会自动重试一次;仍失败时再点一次更新即可补上记录,技能不再被误判为“副本被改过”。关联 #13311。
|
||||
- Fixed: a failed record write after a skill update is retried once; if it still fails, updating again repairs the record instead of leaving the skill marked edited. Refs #13311.
|
||||
@@ -1,2 +0,0 @@
|
||||
- 修复:对话右下角“回到会话顶部/跳到最新”按钮一次点击直达两端;原来的“逐个展开块”跳法改为按住 Shift 点击。关联 #13326、#13229。
|
||||
- Fixed: the jump-to-top and jump-to-latest buttons now go straight to the ends in one click; the old step through expanded blocks is Shift+click. Refs #13326, #13229.
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
// Where the desktop re-declares the kernel's wire types by hand.
|
||||
const (
|
||||
tsWireFile = "desktop/frontend-next/src/port/wire.ts"
|
||||
tsLeaseFile = "desktop/frontend-next/src/port/workspace_lease.ts"
|
||||
tsBoundaryFile = "desktop/frontend-next/src/port/boundary.ts"
|
||||
tsRemoteFile = "desktop/frontend-next/src/port/remote.ts"
|
||||
tsSessionFile = "desktop/frontend-next/src/port/session.ts"
|
||||
@@ -193,7 +194,7 @@ var mirroredWireTypes = []wireMirror{
|
||||
{"internal/state/checkpoint/types.go", "RewindResult", tsSessionFile, "RewindResult"},
|
||||
{"internal/state/checkpoint/types.go", "RewindUndo", tsSessionFile, "RewindUndo"},
|
||||
{"internal/contract/eventwire/wire.go", "ShellExecution", tsWireFile, "Execution"},
|
||||
{"internal/contract/eventwire/workspace_lease.go", "WorkspaceLease", tsWireFile, "WorkspaceLease"},
|
||||
{"internal/contract/eventwire/workspace_lease.go", "WorkspaceLease", tsLeaseFile, "WorkspaceLease"},
|
||||
// A source and a model as the pickers name them. A label the page cannot
|
||||
// read puts the config name back on screen after the user renamed it.
|
||||
{"internal/frontend/serve/providers.go", "providerView", tsProviderFile, "ProviderEntry"},
|
||||
|
||||
Reference in new issue
Block a user