@herbertgao/pi-extensions 2026.10.3 → 2026.10.5
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/README.md +3 -2
- package/THIRD_PARTY_NOTICES.md +4 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +12 -7
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +12 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +67 -16
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +240 -48
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-currency.ts +29 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +92 -59
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/git-stats.ts +162 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/token-rate.ts +192 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +51 -65
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +113 -25
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +160 -24
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +30 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +26 -24
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/sticky-prompt.ts +402 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ask-user-question.ts +173 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -8
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +64 -11
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +41 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +5 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +35 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +101 -31
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +3 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +106 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +9 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +5 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +1 -1
- package/node_modules/pi-provider-qoder/README.md +140 -0
- package/node_modules/pi-provider-qoder/dist/index.js +10249 -0
- package/node_modules/pi-provider-qoder/package.json +57 -0
- package/package.json +5 -2
|
@@ -6,6 +6,7 @@ import { showMoreHintText } from "./show-more-hint.ts";
|
|
|
6
6
|
import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
7
7
|
import { getToolMouseTui } from "../mouse/scroll.ts";
|
|
8
8
|
import { sanitizeToolResultText } from "../../utils/tool-result-sanitize.ts";
|
|
9
|
+
import { oneLine } from "../../utils/format.ts";
|
|
9
10
|
|
|
10
11
|
const TOOL_VIEWPORT_WIDTH_RATIO = 0.8;
|
|
11
12
|
/** 宽屏右侧留白上限:比例留白超过这么多列时改用固定留白。 */
|
|
@@ -75,10 +76,46 @@ export function countLines(text: string): number {
|
|
|
75
76
|
.filter((line) => line.trim().length > 0).length;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* 折叠行的结果文案:`N lines returned`(read 说 loaded)、`Done`、失败时首行错误。
|
|
81
|
+
* 单卡折叠行与工具组的子行共用这一处口径;TaskList 的树形摘要不走这里,由调用方先判。
|
|
82
|
+
*/
|
|
83
|
+
export function collapsedResultSummary(options: {
|
|
84
|
+
result: any;
|
|
85
|
+
toolName: string;
|
|
86
|
+
text: string;
|
|
87
|
+
isError: boolean;
|
|
88
|
+
}): string {
|
|
89
|
+
const { result, toolName, text, isError } = options;
|
|
90
|
+
if (isError) {
|
|
91
|
+
// 72 与原 result.ts 默认一致,保持错误摘要截断宽度。
|
|
92
|
+
return text ? oneLine(text, 72) : "Failed";
|
|
93
|
+
}
|
|
94
|
+
const outputLines = outputLineCount(result) || countLines(text);
|
|
95
|
+
if (!outputLines) return "Done";
|
|
96
|
+
const action = toolName === "read" ? "loaded" : "returned";
|
|
97
|
+
return `${outputLines} ${outputLines === 1 ? "line" : "lines"} ${action}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
78
100
|
function hasExpandableResult(text: string): boolean {
|
|
79
101
|
return countLines(text) > 1;
|
|
80
102
|
}
|
|
81
103
|
|
|
104
|
+
/**
|
|
105
|
+
* 工具耗时:不到 1 秒报毫秒,分钟级起带上分/时。
|
|
106
|
+
* 只报到秒的话,几分钟的长命令会显示成 `300.0s`。
|
|
107
|
+
*/
|
|
108
|
+
export function formatToolDuration(ms: number): string {
|
|
109
|
+
const seconds = ms / 1000;
|
|
110
|
+
if (seconds < 1) return `${Math.round(ms)}ms`;
|
|
111
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
112
|
+
const totalSeconds = Math.floor(seconds);
|
|
113
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
114
|
+
const remainder = totalSeconds % 60;
|
|
115
|
+
if (minutes < 60) return `${minutes}m ${remainder}s`;
|
|
116
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m ${remainder}s`;
|
|
117
|
+
}
|
|
118
|
+
|
|
82
119
|
const activeAnimationContexts = new Set<any>();
|
|
83
120
|
let sharedAnimationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
84
121
|
|
|
@@ -135,6 +172,36 @@ export function scheduleAnimation(
|
|
|
135
172
|
}
|
|
136
173
|
}
|
|
137
174
|
|
|
175
|
+
/**
|
|
176
|
+
* 本回合流式消息里出现过的工具调用。Pi 要等整条 assistant 消息结束才派发执行,
|
|
177
|
+
* 这段窗口工具行已经在等启动,spinner 该逐帧转。
|
|
178
|
+
* 恢复或翻历史重建的旧行不在这个集合里,保持静止。
|
|
179
|
+
*/
|
|
180
|
+
const liveToolCalls = new Set<string>();
|
|
181
|
+
|
|
182
|
+
/** 收下当前流式 assistant 消息里的工具调用 id。 */
|
|
183
|
+
export function noteStreamingToolCalls(message: any): void {
|
|
184
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
185
|
+
for (const block of content) {
|
|
186
|
+
const id = block?.type === "toolCall" ? block.id : undefined;
|
|
187
|
+
if (typeof id === "string" && id) liveToolCalls.add(id);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** 工具收尾后移出等待窗口。 */
|
|
192
|
+
export function retireLiveToolCall(toolCallId: unknown): void {
|
|
193
|
+
if (typeof toolCallId === "string") liveToolCalls.delete(toolCallId);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function clearLiveToolCalls(): void {
|
|
197
|
+
liveToolCalls.clear();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 工具行是否处于等待窗口(流式声明中或等待派发)。 */
|
|
201
|
+
export function isLiveToolCall(toolCallId: unknown): boolean {
|
|
202
|
+
return typeof toolCallId === "string" && liveToolCalls.has(toolCallId);
|
|
203
|
+
}
|
|
204
|
+
|
|
138
205
|
export function pendingIcon(_name: string): string {
|
|
139
206
|
return toolLoadingIcon();
|
|
140
207
|
}
|
|
@@ -231,7 +298,7 @@ export class ExpandedToolResultText {
|
|
|
231
298
|
}
|
|
232
299
|
}
|
|
233
300
|
|
|
234
|
-
/** 截断体末行 `…
|
|
301
|
+
/** 截断体末行 `… N more lines` 旁的展开提示,点击打开全量预览。 */
|
|
235
302
|
export const SHOW_MORE_LABEL = "· click to show more";
|
|
236
303
|
|
|
237
304
|
export type ToolIoSection = "input" | "output";
|
|
@@ -243,14 +310,12 @@ const EXPANDED_TOOL_IO_VIEW_GENERATION = Symbol("ccstyle-expanded-tool-io-view")
|
|
|
243
310
|
/**
|
|
244
311
|
* Expanded tool body with clear Input / Output sections (Grok Build–style).
|
|
245
312
|
*
|
|
246
|
-
* Visual frame
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* flushLeft=true(仅 mode=on 展开卡):去掉树线前导空格,由外层 Box(1,1) 提供 1 格 padding。
|
|
313
|
+
* Visual frame(flushLeft=true:树线的前导空格交给外层 Box(1,1),├/└ 与标题行的勾同列):
|
|
314
|
+
* ├ Input click to show more
|
|
315
|
+
* │ path: src/a.ts
|
|
316
|
+
* │
|
|
317
|
+
* └ Output click to show more
|
|
318
|
+
* result line…
|
|
254
319
|
*
|
|
255
320
|
* Reused across re-renders via context.lastComponent when possible.
|
|
256
321
|
*/
|
|
@@ -281,8 +346,10 @@ export class ExpandedToolIoView {
|
|
|
281
346
|
}
|
|
282
347
|
| undefined;
|
|
283
348
|
|
|
284
|
-
/** flushLeft
|
|
349
|
+
/** flushLeft:贴左渲染(外层 Box 负责 1 格 pad);默认 false 保留 1 格前导空格。 */
|
|
285
350
|
private flushLeft: boolean;
|
|
351
|
+
/** 尾部附注(shell 的耗时)。挂到 Output 的 show-more 行上,没有那一行时单独成行。 */
|
|
352
|
+
private footer: string | undefined;
|
|
286
353
|
|
|
287
354
|
constructor(
|
|
288
355
|
theme: any,
|
|
@@ -398,6 +465,13 @@ export class ExpandedToolIoView {
|
|
|
398
465
|
this.invalidate();
|
|
399
466
|
}
|
|
400
467
|
|
|
468
|
+
/** 尾部附注(耗时等)。挂在 show-more 行上;无 show-more 行时单独成行。 */
|
|
469
|
+
setFooter(text: string | undefined): void {
|
|
470
|
+
if (this.footer === text) return;
|
|
471
|
+
this.footer = text;
|
|
472
|
+
this.invalidate();
|
|
473
|
+
}
|
|
474
|
+
|
|
401
475
|
getHoveredSection(): ToolIoSection | null {
|
|
402
476
|
return this.hoveredSection;
|
|
403
477
|
}
|
|
@@ -405,7 +479,7 @@ export class ExpandedToolIoView {
|
|
|
405
479
|
/** True when the plain truncation footer carries show-more. Input 续行带 │,Output 不带。 */
|
|
406
480
|
matchShowMoreLine(plainLine: string): ToolIoSection | null {
|
|
407
481
|
const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
|
|
408
|
-
if (!line.includes(` · ${showMoreHintText()}`) ||
|
|
482
|
+
if (!line.includes(` · ${showMoreHintText()}`) || !/\d+ more lines/.test(line)) return null;
|
|
409
483
|
if (this.truncated.input && line.includes("│")) return "input";
|
|
410
484
|
if (this.truncated.output) return "output";
|
|
411
485
|
return null;
|
|
@@ -442,7 +516,7 @@ export class ExpandedToolIoView {
|
|
|
442
516
|
|
|
443
517
|
const theme = this.theme;
|
|
444
518
|
const safeWidth = Math.max(1, Math.floor(width));
|
|
445
|
-
// flushLeft:贴左(外层 Box 负责 1 格 pad);否则保留 1
|
|
519
|
+
// flushLeft:贴左(外层 Box 负责 1 格 pad);否则保留 1 格前导空格
|
|
446
520
|
const lead = this.flushLeft ? "" : " ";
|
|
447
521
|
const rail = `${lead}│ `;
|
|
448
522
|
const railWidth = visibleWidth(rail);
|
|
@@ -474,7 +548,14 @@ export class ExpandedToolIoView {
|
|
|
474
548
|
|
|
475
549
|
const pushBody = (
|
|
476
550
|
body: string,
|
|
477
|
-
opts: {
|
|
551
|
+
opts: {
|
|
552
|
+
input?: boolean;
|
|
553
|
+
limit: number;
|
|
554
|
+
continued?: boolean;
|
|
555
|
+
section: ToolIoSection;
|
|
556
|
+
/** 挂在 show-more 行上的附注(shell 耗时),插在展开提示前。 */
|
|
557
|
+
footerNote?: string;
|
|
558
|
+
},
|
|
478
559
|
): boolean /* truncated */ => {
|
|
479
560
|
const raw = body.replace(/\t/g, " ").replace(/\n+$/, "");
|
|
480
561
|
if (!raw.trim()) {
|
|
@@ -502,6 +583,8 @@ export class ExpandedToolIoView {
|
|
|
502
583
|
if (truncated) {
|
|
503
584
|
const hidden = Math.max(0, wrapped.length - visible.length);
|
|
504
585
|
if (hidden > 0) {
|
|
586
|
+
// 省略号已经表达了「还有」,计数前不再加 +。
|
|
587
|
+
const note = opts.footerNote ? theme.fg("dim", ` · ${opts.footerNote}`) : "";
|
|
505
588
|
// hover 只高亮文字,圆点保持 dim(与 group hint 一致)。
|
|
506
589
|
const more =
|
|
507
590
|
theme.fg("dim", " ·") +
|
|
@@ -509,7 +592,7 @@ export class ExpandedToolIoView {
|
|
|
509
592
|
this.hoveredSection === opts.section ? "text" : "dim",
|
|
510
593
|
` ${showMoreHintText()}`,
|
|
511
594
|
);
|
|
512
|
-
pushRailLine(theme.fg("dim", `…
|
|
595
|
+
pushRailLine(theme.fg("dim", `… ${hidden} more lines`) + note + more, opts.continued);
|
|
513
596
|
this.showMoreHeaderRows[opts.section] = lines.length - 1;
|
|
514
597
|
}
|
|
515
598
|
}
|
|
@@ -556,6 +639,7 @@ export class ExpandedToolIoView {
|
|
|
556
639
|
limit: this.maxOutputLines,
|
|
557
640
|
continued: false,
|
|
558
641
|
section: "output",
|
|
642
|
+
footerNote: this.footer,
|
|
559
643
|
});
|
|
560
644
|
} else {
|
|
561
645
|
this.truncated.output = outputWouldTruncate;
|
|
@@ -564,9 +648,16 @@ export class ExpandedToolIoView {
|
|
|
564
648
|
limit: this.maxOutputLines,
|
|
565
649
|
continued: false,
|
|
566
650
|
section: "output",
|
|
651
|
+
footerNote: this.footer,
|
|
567
652
|
});
|
|
568
653
|
}
|
|
569
654
|
|
|
655
|
+
// 耗时挂在 Output 的 show-more 行上(与折叠态一样,提示留在行尾);
|
|
656
|
+
// 没截断就没有那一行,才单独成行。放在最后追加,前面记下的行号不受影响。
|
|
657
|
+
if (this.footer && this.showMoreHeaderRows.output === undefined) {
|
|
658
|
+
pushRailLine(theme.fg("muted", this.footer), false);
|
|
659
|
+
}
|
|
660
|
+
|
|
570
661
|
this.cachedWidth = width;
|
|
571
662
|
this.cachedLines = lines;
|
|
572
663
|
return withIoViewMarkers(this, lines);
|
|
@@ -830,7 +921,7 @@ export function renderExpandedToolResult(
|
|
|
830
921
|
lastComponent?: unknown,
|
|
831
922
|
args?: unknown,
|
|
832
923
|
context?: any,
|
|
833
|
-
/**
|
|
924
|
+
/** 贴左渲染:树线的前导空格交给外层 Box(1,1) */
|
|
834
925
|
flushLeft = false,
|
|
835
926
|
): ExpandedToolIoView | ExpandedToolResultText | Text {
|
|
836
927
|
const { text: inputBody, codeBlock } = formatToolInputBody(args);
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { sanitizeToolResultText } from "./tool-result-sanitize.ts";
|
|
3
3
|
|
|
4
|
+
/** token/计数缩写:<1k 原数,<1M 用 k,≥1M 用 M。footer 与 working message 共用。 */
|
|
5
|
+
export function formatTokenCount(value: number): string {
|
|
6
|
+
if (value < 1000) return `${value}`;
|
|
7
|
+
if (value < 10_000) return `${(value / 1000).toFixed(1)}k`;
|
|
8
|
+
if (value < 1_000_000) return `${Math.round(value / 1000)}k`;
|
|
9
|
+
if (value < 10_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
|
|
10
|
+
return `${Math.round(value / 1_000_000)}M`;
|
|
11
|
+
}
|
|
12
|
+
|
|
4
13
|
/** 毫秒 → "1h 2m 3s"/"2m 3s"/"3s";低于 1 秒返回 ""(省略)。 */
|
|
5
14
|
export function formatDuration(ms: number): string {
|
|
6
15
|
const totalSec = Math.floor(ms / 1000);
|
|
@@ -43,6 +43,11 @@ export const TOOL_GROUPING_GENERATION_KEY = Symbol.for("pi.ccstyle.tool-grouping
|
|
|
43
43
|
// ── regular 主屏渲染补丁 ──
|
|
44
44
|
export const MAIN_SCREEN_DO_RENDER_PATCH = Symbol.for("pi.ccstyle.main-screen-do-render-patch");
|
|
45
45
|
|
|
46
|
+
// ── fullscreen 粘性用户消息头 ──
|
|
47
|
+
export const STICKY_PROMPT_MARKER_PATCH = Symbol.for("pi.ccstyle.sticky-prompt-marker-patch");
|
|
48
|
+
export const STICKY_PROMPT_FRAME_PATCH = Symbol.for("pi.ccstyle.sticky-prompt-frame-patch");
|
|
49
|
+
export const STICKY_PROMPT_STATE_SLOT = Symbol.for("pi.ccstyle.sticky-prompt-state");
|
|
50
|
+
|
|
46
51
|
// ── 鼠标交互 ──
|
|
47
52
|
export const TOOL_MOUSE_OWNER_KEY = Symbol.for("pi.ccstyle.tool-mouse-owner");
|
|
48
53
|
export const TOOL_MOUSE_TUI_SLOT = Symbol.for("pi.ccstyle.tool-mouse-tui");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herbertgao/pi-cc-extensions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "A Pi productivity suite with Claude Code-style UI, context inspection, and agent/session references",
|
|
6
6
|
"type": "module",
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
},
|
|
72
72
|
"x-upstream": {
|
|
73
73
|
"package": "pi-cc-extensions",
|
|
74
|
-
"version": "0.
|
|
75
|
-
"reviewedVersion": "0.
|
|
74
|
+
"version": "1.0.1",
|
|
75
|
+
"reviewedVersion": "1.0.1",
|
|
76
76
|
"repository": "https://github.com/minuque/pi-cc-extensions",
|
|
77
|
-
"commit": "
|
|
77
|
+
"commit": "0188b76f2597772bea3b42075857e030d9a89666"
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# pi-provider-qoder
|
|
2
|
+
|
|
3
|
+
A [pi](https://shittycodingagent.ai/) extension that connects pi to Qoder.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pi install npm:pi-provider-qoder
|
|
7
|
+
# or: omp install npm:pi-provider-qoder
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pi --provider qoder --model Lite
|
|
12
|
+
pi --provider qoder-cn --model Qwen3.7-Plus
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Inside pi:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
/login qoder
|
|
19
|
+
/model Qwen3.8-Max
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Providers
|
|
23
|
+
|
|
24
|
+
Both providers register together.
|
|
25
|
+
|
|
26
|
+
### `qoder` (global)
|
|
27
|
+
|
|
28
|
+
- `https://api3.qoder.sh/`
|
|
29
|
+
- Login: `/login qoder` (browser OAuth or PAT)
|
|
30
|
+
- PAT page: https://qoder.com/account/integrations
|
|
31
|
+
- Env (first match): `QODER_API_KEY`, `QODER_PERSONAL_ACCESS_TOKEN`, `QODER_PAT`
|
|
32
|
+
|
|
33
|
+
### `qoder-cn` (China)
|
|
34
|
+
|
|
35
|
+
- `https://gateway.qoder.com.cn/`
|
|
36
|
+
- Login: `/login qoder-cn` (PAT only)
|
|
37
|
+
- PAT page: https://qoder.com.cn/account/integrations
|
|
38
|
+
- Env (first match): `QODERCN_API_KEY`, `QODERCN_PERSONAL_ACCESS_TOKEN`, `QODERCN_PAT`
|
|
39
|
+
|
|
40
|
+
A PAT (`pt-...`) is exchanged for a job token. Setting any of those env vars logs the provider in at startup.
|
|
41
|
+
|
|
42
|
+
## Models
|
|
43
|
+
|
|
44
|
+
Model IDs are the catalog `display_name` with whitespace stripped. After login, `/model` lists what that region offers.
|
|
45
|
+
|
|
46
|
+
Examples: `Lite`, `Qwen3.8-Max`, `Qwen3.7-Plus`, `Qwen3.8-Flash`.
|
|
47
|
+
|
|
48
|
+
Context uses the largest live catalog option (often 1M). Output is 128K.
|
|
49
|
+
|
|
50
|
+
## Endpoints
|
|
51
|
+
|
|
52
|
+
| | Global (`qoder`) | China (`qoder-cn`) |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| PAT exchange | `https://openapi.qoder.sh/api/v1/jobToken/exchange` | `https://openapi.qoder.com.cn/api/v1/jobToken/exchange` |
|
|
55
|
+
| User info | `https://openapi.qoder.sh/api/v1/userinfo` | `https://openapi.qoder.com.cn/api/v1/userinfo` |
|
|
56
|
+
| Usage | `https://openapi.qoder.sh/api/v2/quota/usage` | `https://openapi.qoder.com.cn/api/v2/quota/usage` |
|
|
57
|
+
| Chat gateway | `https://api3.qoder.sh/` | `https://gateway.qoder.com.cn/` |
|
|
58
|
+
|
|
59
|
+
## Request and account Credits
|
|
60
|
+
|
|
61
|
+
The footer shows **actual session deductions in Credits**, accumulated from Qoder's
|
|
62
|
+
request-level `usage.credits` and `usage.billable` response fields. Tokens are shown
|
|
63
|
+
separately and are not used to estimate Credits. A non-billable request contributes zero
|
|
64
|
+
to actual deductions even when its reported nominal Credits are positive. Missing
|
|
65
|
+
billing metadata is counted as `unknown`, so an incomplete session total is visible.
|
|
66
|
+
Global and CN session totals are separate and are rebuilt from stored session entries,
|
|
67
|
+
including attributed compaction, summary and tool usage entries.
|
|
68
|
+
|
|
69
|
+
Account quota is an **account-wide period balance**, including other Qoder clients.
|
|
70
|
+
Personal, add-on and organization quota are displayed separately. Account balances may
|
|
71
|
+
be rounded or updated later; they do not determine per-request cost.
|
|
72
|
+
|
|
73
|
+
Assistant `usage` preserves Qoder's `credits`, `original_credits`, and `billable`, and
|
|
74
|
+
adds `qoder_provider`, `charged_credits`, and `usd_equivalent`. `charged_credits` is zero
|
|
75
|
+
for non-billable calls, or the returned discounted Credits for billable calls; it remains
|
|
76
|
+
absent when actual deductions cannot be determined. `usd_equivalent` is a reference
|
|
77
|
+
conversion at **$20 / 1,500 Credits**, not a dollar charge. Credits never enter pi's
|
|
78
|
+
dollar-denominated `usage.cost` fields. See the [Qoder usage field definitions](https://docs.qoder.com/cli/sdk/cost-usage).
|
|
79
|
+
|
|
80
|
+
For example, a CN response with `credits: 0.000613272` and `billable: true` contributes
|
|
81
|
+
`0.000613272` Credits, while a Global response with `credits: 0.005221428571428571` and
|
|
82
|
+
`billable: false` contributes zero. The UI keeps up to eight decimal places; serialized
|
|
83
|
+
usage preserves the full returned precision.
|
|
84
|
+
|
|
85
|
+
A compact session-Credits and account-quota status is shown for the selected Qoder provider.
|
|
86
|
+
`/qoder-usage` also shows the session deductions and account quota. For the full
|
|
87
|
+
credit footer, opt in for the current pi session:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
/qoder-usage footer on
|
|
91
|
+
/qoder-usage
|
|
92
|
+
/qoder-usage footer off
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or start pi with `--qoder-credit-footer`. The custom footer replaces the misleading
|
|
96
|
+
`$0.000 (sub)` with labelled session Credits and account-quota rows. Input/output,
|
|
97
|
+
cache totals/hit rate, context, model/thinking level, directory, branch, session name,
|
|
98
|
+
and other extension statuses remain visible. Existing nonzero dollar charges in mixed
|
|
99
|
+
sessions are retained as `session $…` and are not relabelled as Qoder credits.
|
|
100
|
+
Narrow terminals wrap quota and put the model on its own line.
|
|
101
|
+
|
|
102
|
+
pi exposes one custom-footer slot. Enabling this option replaces another custom footer;
|
|
103
|
+
leave it off when using a separate footer extension. If another extension takes the slot
|
|
104
|
+
later, Qoder stops trying to reclaim it. Disabling restores pi's built-in footer only
|
|
105
|
+
while Qoder still owns the slot. The public API does not expose the current auto-compaction
|
|
106
|
+
setting, so the custom footer does not add the built-in `(auto)` hint. Alternate hosts without
|
|
107
|
+
pi's TUI footer API keep the status/command where supported.
|
|
108
|
+
|
|
109
|
+
Quota refreshes asynchronously at session start, model selection, before a new agent run,
|
|
110
|
+
and after an agent run. The in-memory cache lasts 30 seconds and coalesces simultaneous
|
|
111
|
+
requests; agent completion and `/qoder-usage` force a refresh. A local timer marks expired snapshots `[stale]`
|
|
112
|
+
without polling. Missing data shows `?`/unavailable, not zero; failed refreshes retain only
|
|
113
|
+
the last snapshot for the same account. Personal, add-on and organization quota are separate,
|
|
114
|
+
with the API's own units. The command also shows the allowance and expiry when provided.
|
|
115
|
+
Credential lookup and quota requests are each bounded to 10 seconds.
|
|
116
|
+
|
|
117
|
+
There is no dedicated host login/logout event. After changing authentication for an
|
|
118
|
+
already-selected model, run `/qoder-usage`; otherwise the next supported lifecycle event
|
|
119
|
+
rechecks identity. No account quota is written to disk. Per-request Credits are stored
|
|
120
|
+
with the assistant usage in the host's session log.
|
|
121
|
+
|
|
122
|
+
### Offline visual preview
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm ci
|
|
126
|
+
npm run demo:credits
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The demo runs the same footer renderer inside pi-tui with **sample data only**. It makes
|
|
130
|
+
no account or model requests. Press `f` (fresh), `s` (stale), `u` (unavailable), `0` (zero
|
|
131
|
+
quota), or `q` (quit). It shows both standard and 54-column layouts. Global and CN
|
|
132
|
+
quota queries and request-level billing fields have been verified with live calls.
|
|
133
|
+
|
|
134
|
+
Sanitized billing samples in `src/__fixtures__/billing/usage.json` cover free Global
|
|
135
|
+
calls, paid Global/CN calls, and two Kimi-K3 calls totaling 1.03185159 Credits.
|
|
136
|
+
Session accounting regression tests use these samples without making live requests.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|