@aibyzero/byz 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.9 - 2026-08-30
6
+
7
+ ### Changed
8
+
9
+ - Changed the default BYZ terminal footer to show the project name, git branch, remaining context percentage, token usage, cost, extension statuses, and current model.
10
+ - Changed long-running BYZ waits to show a compact non-technical progress card by default, with the fuller work-site card available in details mode.
11
+ - Added persistent BYZ detail-mode preferences through `/details remember` and `/details remember compact`.
12
+
5
13
  ## 0.1.7 - 2026-08-29
6
14
 
7
15
  ### Added
@@ -1,31 +1,281 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, join } from "node:path";
1
4
  import { createInteractionPolicy, formatDecision, parseConversationControl } from "./interaction-policy.js";
2
5
  import { createRoutingPolicy } from "./routing-policy.js";
3
6
 
4
7
  const WELCOME = "BYZ\n\n你想让我帮你做什么?";
8
+ const DETAIL_MODE_COMPACT = "compact";
9
+ const DETAIL_MODE_DETAILS = "details";
5
10
 
6
- export function createConversationExtension() {
11
+ function getByzAgentDir() {
12
+ return process.env.BYZ_CODING_AGENT_DIR || join(homedir(), ".byz", "agent");
13
+ }
14
+
15
+ function getConversationConfigPath() {
16
+ return join(getByzAgentDir(), "conversation.json");
17
+ }
18
+
19
+ function readConversationConfig() {
20
+ try {
21
+ return JSON.parse(readFileSync(getConversationConfigPath(), "utf8"));
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+
27
+ function getSavedDetailMode() {
28
+ const mode = readConversationConfig().detailMode;
29
+ return mode === DETAIL_MODE_DETAILS ? DETAIL_MODE_DETAILS : DETAIL_MODE_COMPACT;
30
+ }
31
+
32
+ function saveDetailMode(mode) {
33
+ const configPath = getConversationConfigPath();
34
+ mkdirSync(dirname(configPath), { recursive: true });
35
+ writeFileSync(configPath, `${JSON.stringify({ ...readConversationConfig(), detailMode: mode }, null, "\t")}\n`);
36
+ }
37
+
38
+ function formatTokens(count) {
39
+ if (count < 1000) return String(count);
40
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
41
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
42
+ if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
43
+ return `${Math.round(count / 1000000)}M`;
44
+ }
45
+
46
+ function truncateText(text, width) {
47
+ if (width <= 0) return "";
48
+ if (text.length <= width) return text;
49
+ if (width <= 1) return "…".slice(0, width);
50
+ return `${text.slice(0, width - 1)}…`;
51
+ }
52
+
53
+ function findProjectRoot(cwd) {
54
+ let dir = cwd;
55
+ while (dir) {
56
+ if (existsSync(join(dir, ".git"))) return dir;
57
+ const parent = dirname(dir);
58
+ if (parent === dir) return cwd;
59
+ dir = parent;
60
+ }
61
+ return cwd;
62
+ }
63
+
64
+ function getProjectName(cwd) {
65
+ return basename(findProjectRoot(cwd)) || basename(cwd) || cwd;
66
+ }
67
+
68
+ function addUsage(totals, usage) {
69
+ if (!usage) return;
70
+ totals.input += usage.input ?? 0;
71
+ totals.output += usage.output ?? 0;
72
+ totals.cacheRead += usage.cacheRead ?? 0;
73
+ totals.cacheWrite += usage.cacheWrite ?? 0;
74
+ totals.cost += usage.cost?.total ?? 0;
75
+ }
76
+
77
+ function getUsageTotals(ctx) {
78
+ const totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
79
+ for (const entry of ctx.sessionManager?.getEntries?.() ?? []) {
80
+ if (entry.type === "message" && entry.message?.role === "assistant") {
81
+ addUsage(totals, entry.message.usage);
82
+ } else if (entry.type === "message" && entry.message?.role === "toolResult") {
83
+ addUsage(totals, entry.message.usage);
84
+ } else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
85
+ addUsage(totals, entry.usage);
86
+ }
87
+ }
88
+ return totals;
89
+ }
90
+
91
+ function shortModelName(modelId) {
92
+ if (!modelId) return "no-model";
93
+ return modelId
94
+ .replace(/^claude-/, "")
95
+ .replace(/^gpt-/, "gpt-")
96
+ .replace(/-20\d{6}$/, "")
97
+ .replace(/-latest$/, "");
98
+ }
99
+
100
+ function createByzFooter(ctx, tui, theme, footerData) {
101
+ const unsubscribe = footerData.onBranchChange?.(() => tui.requestRender?.());
102
+ return {
103
+ invalidate() {},
104
+ dispose() {
105
+ unsubscribe?.();
106
+ },
107
+ render(width) {
108
+ const safeWidth = Math.max(1, width ?? 80);
109
+ const cwd = ctx.sessionManager?.getCwd?.() ?? ctx.cwd ?? process.cwd();
110
+ const parts = [getProjectName(cwd)];
111
+ const branch = footerData.getGitBranch?.();
112
+ if (branch) parts.push(branch);
113
+
114
+ const contextUsage = ctx.getContextUsage?.();
115
+ if (contextUsage) {
116
+ const left = contextUsage.percent === null ? "?" : `${Math.max(0, 100 - contextUsage.percent).toFixed(0)}%`;
117
+ parts.push(`left ${left}`);
118
+ }
119
+
120
+ const usage = getUsageTotals(ctx);
121
+ if (usage.input > 0) parts.push(`↑${formatTokens(usage.input)}`);
122
+ if (usage.output > 0) parts.push(`↓${formatTokens(usage.output)}`);
123
+ if (usage.cacheRead > 0) parts.push(`R${formatTokens(usage.cacheRead)}`);
124
+ if (usage.cacheWrite > 0) parts.push(`W${formatTokens(usage.cacheWrite)}`);
125
+ if (usage.cost > 0) parts.push(`$${usage.cost.toFixed(3)}`);
126
+
127
+ const extensionStatuses = footerData.getExtensionStatuses?.();
128
+ for (const text of extensionStatuses?.values?.() ?? []) {
129
+ const clean = String(text)
130
+ .replace(/[\r\n\t]/g, " ")
131
+ .replace(/ +/g, " ")
132
+ .trim();
133
+ if (clean) parts.push(clean);
134
+ }
135
+
136
+ const leftText = parts.join(" ");
137
+ const modelText = shortModelName(ctx.model?.id);
138
+ const minGap = 2;
139
+ let line;
140
+ if (leftText.length + minGap + modelText.length <= safeWidth) {
141
+ line = `${leftText}${" ".repeat(safeWidth - leftText.length - modelText.length)}${modelText}`;
142
+ } else {
143
+ const modelBudget = Math.min(modelText.length, Math.max(0, safeWidth - minGap - 12));
144
+ const model = truncateText(modelText, modelBudget);
145
+ const leftBudget = Math.max(1, safeWidth - minGap - model.length);
146
+ line = `${truncateText(leftText, leftBudget)}${" ".repeat(Math.max(minGap, safeWidth - leftBudget - model.length))}${model}`;
147
+ }
148
+ return [theme.fg?.("dim", line) ?? line];
149
+ },
150
+ };
151
+ }
152
+
153
+ function createProgressState() {
154
+ return {
155
+ goal: "当前任务",
156
+ stage: "确认目标与边界",
157
+ confirmed: [],
158
+ judgements: [],
159
+ nextSteps: ["完成必要检查", "整理结果给你"],
160
+ safeguards: ["不会提交代码", "不会执行高影响动作"],
161
+ tools: { inspected: 0, edited: 0, commands: 0 },
162
+ visible: false,
163
+ };
164
+ }
165
+
166
+ function pushUnique(list, value, limit = 3) {
167
+ if (!value || list.includes(value)) return;
168
+ list.push(value);
169
+ if (list.length > limit) list.shift();
170
+ }
171
+
172
+ function summarizeGoal(prompt) {
173
+ const clean = String(prompt ?? "")
174
+ .replace(/[\r\n\t]/g, " ")
175
+ .replace(/展开细节[,,;;\s]*/g, "")
176
+ .replace(/ +/g, " ")
177
+ .trim();
178
+ if (!clean) return "当前任务";
179
+ return clean.length > 28 ? `${clean.slice(0, 27)}…` : clean;
180
+ }
181
+
182
+ function getActivitySummary(state) {
183
+ const activity = [];
184
+ if (state.tools.inspected > 0) activity.push(`查看 ${state.tools.inspected} 项`);
185
+ if (state.tools.edited > 0) activity.push(`修改 ${state.tools.edited} 项`);
186
+ if (state.tools.commands > 0) activity.push(`命令 ${state.tools.commands} 次`);
187
+ return activity.join(",");
188
+ }
189
+
190
+ function renderProgressCard(state, options = {}) {
191
+ const activity = getActivitySummary(state);
192
+ if (options.compact) {
193
+ const progress = activity || state.confirmed.at(-1) || state.stage;
194
+ const next = state.nextSteps.at(-1) ?? "整理结果给你";
195
+ const boundary = state.safeguards.at(-1) ?? "不会提交代码";
196
+ return [`处理中:${state.goal}`, `进展:${progress}`, `下一步:${next}`, `边界:${boundary}`].join("\n");
197
+ }
198
+
199
+ const lines = [`正在处理:${state.goal}`, `当前阶段:${state.stage}`];
200
+ if (activity) lines.push(`现场进展:${activity}`);
201
+ if (state.confirmed.length > 0) lines.push(`已确认:${state.confirmed.join(";")}`);
202
+ if (state.judgements.length > 0) lines.push(`当前判断:${state.judgements.join(";")}`);
203
+ if (state.nextSteps.length > 0) lines.push(`下一步:${state.nextSteps.join(";")}`);
204
+ if (state.safeguards.length > 0) lines.push(`不会做:${state.safeguards.join(";")}`);
205
+ return lines.join("\n");
206
+ }
207
+
208
+ function stageForTool(toolName) {
209
+ if (["read", "grep", "find", "ls"].includes(toolName)) return "定位和核对相关材料";
210
+ if (["edit", "write"].includes(toolName)) return "执行最小必要修改";
211
+ if (["bash", "powershell"].includes(toolName)) return "运行命令并核对结果";
212
+ return "处理必要步骤";
213
+ }
214
+
215
+ function updateProgressFromToolStart(state, toolName) {
216
+ state.stage = stageForTool(toolName);
217
+ if (["read", "grep", "find", "ls"].includes(toolName)) {
218
+ pushUnique(state.nextSteps, "基于证据判断方案");
219
+ } else if (["edit", "write"].includes(toolName)) {
220
+ pushUnique(state.judgements, "优先做小改动,避免扩大范围");
221
+ pushUnique(state.nextSteps, "补充验证");
222
+ } else if (["bash", "powershell"].includes(toolName)) {
223
+ pushUnique(state.nextSteps, "根据命令结果决定是否继续");
224
+ }
225
+ }
226
+
227
+ function updateProgressFromToolEnd(state, toolName, isError) {
228
+ if (["read", "grep", "find", "ls"].includes(toolName)) {
229
+ state.tools.inspected += 1;
230
+ pushUnique(state.confirmed, "已查看相关项目资料");
231
+ } else if (["edit", "write"].includes(toolName)) {
232
+ state.tools.edited += 1;
233
+ pushUnique(state.confirmed, isError ? "修改步骤需要复核" : "已完成代码层面的变更");
234
+ } else if (["bash", "powershell"].includes(toolName)) {
235
+ state.tools.commands += 1;
236
+ pushUnique(state.confirmed, isError ? "命令结果需要处理" : "已执行验证命令");
237
+ }
238
+ if (isError) {
239
+ state.stage = "处理异常结果";
240
+ pushUnique(state.judgements, "先解释失败原因,再决定是否调整");
241
+ return;
242
+ }
243
+ state.stage = "继续核对并收敛结果";
244
+ }
245
+
246
+ export function createConversationExtension(options = {}) {
7
247
  const policy = createInteractionPolicy();
8
248
  const routingPolicy = createRoutingPolicy();
249
+ const progressCardDelayMs = options.progressCardDelayMs ?? 8_000;
250
+ let savedDetailMode = getSavedDetailMode();
9
251
 
10
252
  return function conversationExtension(pi) {
11
253
  let progressTimer;
254
+ let progressState = createProgressState();
255
+ let activeCtx;
12
256
 
13
257
  function clearProgressTimer() {
14
258
  if (progressTimer) clearTimeout(progressTimer);
15
259
  progressTimer = undefined;
16
260
  }
17
261
 
262
+ function publishProgress() {
263
+ if (!activeCtx) return;
264
+ progressState.visible = true;
265
+ activeCtx.ui.setWorkingMessage?.(renderProgressCard(progressState, { compact: !policy.isDetailEnabled() }));
266
+ }
267
+
268
+ function updateVisibleProgress() {
269
+ if (progressState.visible) publishProgress();
270
+ }
271
+
18
272
  pi.on("session_start", (_event, ctx) => {
19
273
  routingPolicy.reset();
274
+ policy.setDetailEnabled(savedDetailMode === DETAIL_MODE_DETAILS);
20
275
  ctx.ui.setTitle?.("BYZ");
21
276
  ctx.ui.setMessagePresenter?.((message) => policy.presentAssistantMessage(message));
22
- ctx.ui.setToolExecutionVisible?.(false);
23
- ctx.ui.setFooter?.(() => ({
24
- invalidate() {},
25
- render() {
26
- return [];
27
- },
28
- }));
277
+ ctx.ui.setToolExecutionVisible?.(policy.isDetailEnabled());
278
+ ctx.ui.setFooter?.((tui, theme, footerData) => createByzFooter(ctx, tui, theme, footerData));
29
279
  ctx.ui.setConfirmationPresenter?.(async ({ title, message, confirm }) => {
30
280
  const prompt = formatDecision({
31
281
  impact: message,
@@ -42,33 +292,100 @@ export function createConversationExtension() {
42
292
  ctx.ui.notify(WELCOME, "info");
43
293
  });
44
294
  pi.on("agent_start", (_event, ctx) => {
295
+ activeCtx = ctx;
45
296
  policy.resetProgress();
297
+ progressState.visible = false;
46
298
  clearProgressTimer();
299
+ ctx.ui.setWorkingMessage?.("正在确认目标与边界…");
47
300
  progressTimer = setTimeout(() => {
48
- const message = policy.present("progress", "");
49
- if (message) ctx.ui.setWorkingMessage?.(message);
50
- }, 30_000);
301
+ publishProgress();
302
+ }, progressCardDelayMs);
303
+ });
304
+ pi.on("tool_execution_start", (event) => {
305
+ updateProgressFromToolStart(progressState, event.toolName);
306
+ updateVisibleProgress();
307
+ });
308
+ pi.on("tool_execution_end", (event) => {
309
+ updateProgressFromToolEnd(progressState, event.toolName, event.isError);
310
+ updateVisibleProgress();
311
+ });
312
+ pi.on("message_update", (event) => {
313
+ if (event.message?.role !== "assistant") return;
314
+ progressState.stage = "组织回复";
315
+ pushUnique(progressState.nextSteps, "给出结论和已做验证");
316
+ updateVisibleProgress();
51
317
  });
52
318
  pi.on("agent_end", () => {
53
319
  clearProgressTimer();
320
+ activeCtx?.ui.setWorkingMessage?.();
321
+ activeCtx = undefined;
54
322
  });
55
323
  pi.on("session_shutdown", () => {
56
324
  routingPolicy.reset();
57
325
  clearProgressTimer();
326
+ activeCtx?.ui.setWorkingMessage?.();
327
+ activeCtx = undefined;
58
328
  });
59
- function showDetails(ctx) {
60
- policy.setDetailEnabled(true);
61
- ctx.ui.setToolExecutionVisible?.(true);
62
- ctx.ui.notify("已展开细节。高级控制:/fast、/prewalk、/workflow。", "info");
329
+ function applyDetailMode(ctx, mode, options = {}) {
330
+ policy.setDetailEnabled(mode === DETAIL_MODE_DETAILS);
331
+ ctx.ui.setToolExecutionVisible?.(policy.isDetailEnabled());
332
+ if (options.remember) {
333
+ saveDetailMode(mode);
334
+ savedDetailMode = mode;
335
+ }
336
+ if (mode === DETAIL_MODE_DETAILS) {
337
+ const scope = options.remember ? "已设为所有会话默认" : "仅当前会话";
338
+ ctx.ui.notify(`已展开细节(${scope})。高级控制:/fast、/prewalk、/workflow。`, "info");
339
+ return;
340
+ }
341
+ const scope = options.remember ? "已设为所有会话默认" : "仅当前会话";
342
+ ctx.ui.notify(`已切回紧凑模式(${scope})。`, "info");
343
+ }
344
+
345
+ function handleDetailsCommand(args, ctx) {
346
+ const action = String(args ?? "")
347
+ .trim()
348
+ .toLowerCase();
349
+ if (!action || action === "on") {
350
+ applyDetailMode(ctx, DETAIL_MODE_DETAILS);
351
+ return;
352
+ }
353
+ if (["off", "compact"].includes(action)) {
354
+ applyDetailMode(ctx, DETAIL_MODE_COMPACT);
355
+ return;
356
+ }
357
+ if (["remember", "save", "details"].includes(action)) {
358
+ applyDetailMode(ctx, DETAIL_MODE_DETAILS, { remember: true });
359
+ return;
360
+ }
361
+ if (["remember compact", "save compact", "compact remember"].includes(action)) {
362
+ applyDetailMode(ctx, DETAIL_MODE_COMPACT, { remember: true });
363
+ return;
364
+ }
365
+ if (action === "status") {
366
+ const current = policy.isDetailEnabled() ? DETAIL_MODE_DETAILS : DETAIL_MODE_COMPACT;
367
+ ctx.ui.notify(`当前:${current}。默认:${savedDetailMode}。`, "info");
368
+ return;
369
+ }
370
+ ctx.ui.notify("用法:/details [on|off|remember|remember compact|status]", "warning");
63
371
  }
64
372
 
65
373
  pi.registerCommand("details", {
66
- description: "Show BYZ advanced controls",
67
- handler: async (_args, ctx) => showDetails(ctx),
374
+ description: "Configure BYZ detail mode",
375
+ handler: async (args, ctx) => handleDetailsCommand(args, ctx),
68
376
  });
69
377
  pi.on("before_agent_start", async (event, ctx) => {
70
378
  const route = routingPolicy.route(event.prompt);
71
- if (route.details || parseConversationControl(event.prompt) === "detail") showDetails(ctx);
379
+ progressState = createProgressState();
380
+ progressState.goal = summarizeGoal(event.prompt);
381
+ pushUnique(progressState.confirmed, "已收到目标");
382
+ if (route.kind !== "general") pushUnique(progressState.judgements, `任务类型:${route.kind}`);
383
+ if (route.preferences.autonomy === "confirm-key-actions") {
384
+ pushUnique(progressState.safeguards, "关键动作会先确认");
385
+ }
386
+ if (route.details || parseConversationControl(event.prompt) === "detail") {
387
+ applyDetailMode(ctx, DETAIL_MODE_DETAILS);
388
+ }
72
389
  if (policy.isDetailEnabled()) {
73
390
  ctx.ui.notify(
74
391
  `当前类别:${route.kind}。当前偏好:主动程度 ${route.preferences.autonomy},交付 ${route.preferences.delivery}。`,