@zzs-fun/current-question 0.1.1 → 0.1.2
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 +6 -2
- package/README.zh-CN.md +5 -1
- package/index.ts +42 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# current-question
|
|
2
2
|
|
|
3
|
-
[English](./README.md) | [
|
|
3
|
+
[English](./README.md) | [Chinese (Simplified)](./README.zh-CN.md)
|
|
4
4
|
|
|
5
5
|
> A [pi coding agent](https://pi.dev) extension that pins your recent questions above the editor — navigate, expand, copy, and reply with context.
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
- Shows the last N user questions (default 5; set with `/current-question <number>`)
|
|
12
12
|
- One line per question; long lines are truncated with `...` (correctly handles double-width CJK characters)
|
|
13
|
-
- Title carries the round counter: `❯
|
|
13
|
+
- Title carries the round counter: `❯ Current Questions (N)` where N is the current question round
|
|
14
14
|
- Collapsed by default; `Ctrl+Alt+L` expands the selected entry to show the full question + its answer, `Ctrl+Alt+H` collapses
|
|
15
15
|
- Collapsed: `Ctrl+Alt+H` hides the widget; hidden: `Ctrl+Alt+L` shows it again
|
|
16
16
|
- Expanded: `Ctrl+Alt+J/K` scrolls the answer; collapsed: `Ctrl+Alt+J/K` switches entries
|
|
@@ -26,7 +26,11 @@
|
|
|
26
26
|
## Install
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
+
# Install globally
|
|
29
30
|
pi install npm:@zzs-fun/current-question
|
|
31
|
+
|
|
32
|
+
# Or install for the current project only
|
|
33
|
+
pi install -l npm:@zzs-fun/current-question
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
## Shortcuts
|
package/README.zh-CN.md
CHANGED
|
@@ -10,7 +10,7 @@ pi coding agent 扩展,在编辑器上方固定显示「最近 n 条用户问
|
|
|
10
10
|
|
|
11
11
|
- 显示最近 N 条用户问题(默认 5 条,`/current-question <数字>` 设置)
|
|
12
12
|
- 每条只展示一行,超长自动截断并加 `...`(正确处理中文等双宽字符)
|
|
13
|
-
- 标题带「第 N 轮」计数:`❯
|
|
13
|
+
- 标题带「第 N 轮」计数:`❯ Current Questions (N)`,N 为当前提问轮数
|
|
14
14
|
- 默认收起;选中条可 `Ctrl+Alt+L` 展开看问题全文 + 该轮回答,`Ctrl+Alt+H` 收起
|
|
15
15
|
- 收起态 `Ctrl+Alt+H` 隐藏当前问题;隐藏态 `Ctrl+Alt+L` 重新显示
|
|
16
16
|
- 展开态 `Ctrl+Alt+J/K` 滚动回答内容;收起态 `Ctrl+Alt+J/K` 切换问题条目
|
|
@@ -26,7 +26,11 @@ pi coding agent 扩展,在编辑器上方固定显示「最近 n 条用户问
|
|
|
26
26
|
## 安装
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
+
# 全局安装
|
|
29
30
|
pi install npm:@zzs-fun/current-question
|
|
31
|
+
|
|
32
|
+
# 或仅安装到当前项目
|
|
33
|
+
pi install -l npm:@zzs-fun/current-question
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
## 快捷键
|
package/index.ts
CHANGED
|
@@ -47,9 +47,9 @@ const MAX_QUESTION_LINES = 6;
|
|
|
47
47
|
const WIDGET_HEIGHT_RESERVED = 10;
|
|
48
48
|
|
|
49
49
|
/** 外部编辑器分节标记(行首严格匹配) */
|
|
50
|
-
const MARKER_QUESTION = ">>
|
|
51
|
-
const MARKER_ANSWER = ">>
|
|
52
|
-
const MARKER_REPLY = ">>
|
|
50
|
+
const MARKER_QUESTION = ">> Question";
|
|
51
|
+
const MARKER_ANSWER = ">> Answer";
|
|
52
|
+
const MARKER_REPLY = ">> Reply";
|
|
53
53
|
|
|
54
54
|
/** 缓存的 TUI 实例句柄,供外部编辑器流程暂停/恢复终端 */
|
|
55
55
|
interface TuiHandle {
|
|
@@ -75,7 +75,7 @@ const NVIM_LOCK_SCRIPT = [
|
|
|
75
75
|
'" —— 2. 找到最后一个 “>> 回复” 标记行 ——',
|
|
76
76
|
'let g:pi_ctx_locked_end = 0',
|
|
77
77
|
'for s:i in range(1, line(\'$\'))',
|
|
78
|
-
' if getline(s:i) =~# \'^>>\\s
|
|
78
|
+
' if getline(s:i) =~# \'^>>\\s*Reply\\s*$\'',
|
|
79
79
|
' let g:pi_ctx_locked_end = s:i',
|
|
80
80
|
' endif',
|
|
81
81
|
'endfor',
|
|
@@ -89,7 +89,7 @@ const NVIM_LOCK_SCRIPT = [
|
|
|
89
89
|
'function! PiCtxGuardInsert() abort',
|
|
90
90
|
' if line(\'.\') <= g:pi_ctx_locked_end',
|
|
91
91
|
' call feedkeys("\\<Esc>", \'n\')',
|
|
92
|
-
' echohl WarningMsg | echo "
|
|
92
|
+
' echohl WarningMsg | echo "The question and answer history is read-only. Edit below \'>> Reply\'." | echohl None',
|
|
93
93
|
' endif',
|
|
94
94
|
'endfunction',
|
|
95
95
|
'autocmd InsertEnter <buffer> call PiCtxGuardInsert()',
|
|
@@ -100,7 +100,7 @@ const NVIM_LOCK_SCRIPT = [
|
|
|
100
100
|
' let g:pi_ctx_restoring = 1',
|
|
101
101
|
' undo',
|
|
102
102
|
' unlet g:pi_ctx_restoring',
|
|
103
|
-
' echohl WarningMsg | echo "
|
|
103
|
+
' echohl WarningMsg | echo "Restored the modified question and answer history." | echohl None',
|
|
104
104
|
' endif',
|
|
105
105
|
'endfunction',
|
|
106
106
|
'autocmd TextChanged,TextChangedI <buffer> call PiCtxCheckLocked()',
|
|
@@ -135,7 +135,7 @@ function extractText(content: unknown): string {
|
|
|
135
135
|
}
|
|
136
136
|
const text = parts.join("").trim();
|
|
137
137
|
if (text) return text;
|
|
138
|
-
return hasImage ? "[
|
|
138
|
+
return hasImage ? "[Image]" : "";
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/**
|
|
@@ -220,7 +220,7 @@ function resolveEditorCommand(): { command: string; args: string[] } {
|
|
|
220
220
|
* 回复区放当前编辑器文本,前两节作为只读参考。
|
|
221
221
|
*/
|
|
222
222
|
function buildContextFileContent(q: { text: string; answer: string }, replyText: string): string {
|
|
223
|
-
const answer = q.answer?.trim() || "(
|
|
223
|
+
const answer = q.answer?.trim() || "(No answer yet)";
|
|
224
224
|
return `${MARKER_QUESTION}\n\n${q.text}\n\n${MARKER_ANSWER}\n\n${answer}\n\n${MARKER_REPLY}\n\n${replyText}\n`;
|
|
225
225
|
}
|
|
226
226
|
|
|
@@ -238,7 +238,7 @@ function parseReplyFromContent(content: string): { reply: string; markerFound: b
|
|
|
238
238
|
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
239
239
|
let markerIdx = -1;
|
|
240
240
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
241
|
-
if (/^>>\s
|
|
241
|
+
if (/^>>\s*Reply\s*$/.test(lines[i]!)) {
|
|
242
242
|
markerIdx = i;
|
|
243
243
|
break;
|
|
244
244
|
}
|
|
@@ -322,17 +322,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
322
322
|
const copyQuestionAndAnswer = async (ctx: ExtensionContext) => {
|
|
323
323
|
const q = getSelectedQuestion();
|
|
324
324
|
if (!q) {
|
|
325
|
-
ctx.ui.notify("
|
|
325
|
+
ctx.ui.notify("No question history to copy", "warning");
|
|
326
326
|
return;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
const text = `>>
|
|
329
|
+
const text = `>> Question\n\n${q.text}\n\n>> Answer\n\n${q.answer || "(No answer yet)"}`;
|
|
330
330
|
try {
|
|
331
331
|
await copyToClipboard(text);
|
|
332
|
-
ctx.ui.notify(
|
|
332
|
+
ctx.ui.notify(`Copied question and answer from round ${q.round}`, "info");
|
|
333
333
|
} catch (error) {
|
|
334
|
-
console.error("[current-question]
|
|
335
|
-
ctx.ui.notify(
|
|
334
|
+
console.error("[current-question] Failed to copy to clipboard", error);
|
|
335
|
+
ctx.ui.notify(`Copy failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
336
336
|
}
|
|
337
337
|
};
|
|
338
338
|
|
|
@@ -406,12 +406,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
406
406
|
const indentWidth = visibleWidth(indent);
|
|
407
407
|
const n = recentQuestions.length;
|
|
408
408
|
const out: string[] = [];
|
|
409
|
-
out.push(theme.fg("accent", theme.bold(`❯
|
|
409
|
+
out.push(theme.fg("accent", theme.bold(`❯ Current Questions (${currentRound})`)));
|
|
410
410
|
|
|
411
411
|
// 顶部指示:窗口上方还有更新的问题
|
|
412
412
|
const newerCount = n - (windowStart + maxLines);
|
|
413
413
|
if (newerCount > 0) {
|
|
414
|
-
out.push(`${theme.fg("dim", indent)}${theme.fg("dim", `↑
|
|
414
|
+
out.push(`${theme.fg("dim", indent)}${theme.fg("dim", `↑ ${newerCount} newer`)}`);
|
|
415
415
|
}
|
|
416
416
|
|
|
417
417
|
// 可见窗口 [windowStart, windowStart+maxLines),反转使最新在上
|
|
@@ -445,9 +445,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
445
445
|
}
|
|
446
446
|
});
|
|
447
447
|
if (qTruncated) {
|
|
448
|
-
out.push(`${theme.fg("dim", bodyIndent)}${theme.fg("dim", `…
|
|
448
|
+
out.push(`${theme.fg("dim", bodyIndent)}${theme.fg("dim", `… Question has ${rawQLines.length} lines; showing first ${MAX_QUESTION_LINES} (Ctrl+Alt+B copies all)`)}`);
|
|
449
449
|
}
|
|
450
|
-
out.push(`${theme.fg("dim", bodyIndent)}${theme.fg("accent", "─
|
|
450
|
+
out.push(`${theme.fg("dim", bodyIndent)}${theme.fg("accent", "─ Answer ─")}`);
|
|
451
451
|
if (q.answer) {
|
|
452
452
|
// 全部分行并缓存,供滚动 handler 做边界判断
|
|
453
453
|
cachedAnswerLines = wrapText(q.answer, bodyWidth);
|
|
@@ -471,7 +471,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
471
471
|
const viewEnd = Math.min(total, answerScroll + answerView);
|
|
472
472
|
// 回答上方滚动提示
|
|
473
473
|
if (answerScroll > 0) {
|
|
474
|
-
out.push(`${theme.fg("dim", bodyIndent)}${theme.fg("dim", `↑
|
|
474
|
+
out.push(`${theme.fg("dim", bodyIndent)}${theme.fg("dim", `↑ ${answerScroll} lines above (Ctrl+Alt+K)`)}`);
|
|
475
475
|
}
|
|
476
476
|
for (const ln of cachedAnswerLines.slice(answerScroll, viewEnd)) {
|
|
477
477
|
out.push(`${theme.fg("dim", bodyIndent)}${theme.fg("muted", ln)}`);
|
|
@@ -479,10 +479,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
479
479
|
// 回答下方滚动提示
|
|
480
480
|
const remain = total - viewEnd;
|
|
481
481
|
if (remain > 0) {
|
|
482
|
-
out.push(`${theme.fg("dim", bodyIndent)}${theme.fg("dim", `↓
|
|
482
|
+
out.push(`${theme.fg("dim", bodyIndent)}${theme.fg("dim", `↓ ${remain} lines below (Ctrl+Alt+J)`)}`);
|
|
483
483
|
}
|
|
484
484
|
} else {
|
|
485
|
-
out.push(`${theme.fg("dim", bodyIndent)}${theme.fg("dim", "(
|
|
485
|
+
out.push(`${theme.fg("dim", bodyIndent)}${theme.fg("dim", "(No answer yet)")}`);
|
|
486
486
|
}
|
|
487
487
|
}
|
|
488
488
|
} else {
|
|
@@ -496,14 +496,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
496
496
|
// 底部指示:窗口下方还有更早的问题
|
|
497
497
|
const olderCount = windowStart;
|
|
498
498
|
if (olderCount > 0) {
|
|
499
|
-
out.push(`${theme.fg("dim", indent)}${theme.fg("dim", `↓
|
|
499
|
+
out.push(`${theme.fg("dim", indent)}${theme.fg("dim", `↓ ${olderCount} older`)}`);
|
|
500
500
|
}
|
|
501
501
|
// 兜底:极端配置或小终端下,若 widget 总行数仍超上限,从底部截断并提示,
|
|
502
502
|
// 确保不触碰终端顶部引发流式抖动。
|
|
503
503
|
if (out.length > maxWidgetRows) {
|
|
504
504
|
const overflow = out.length - maxWidgetRows;
|
|
505
505
|
out.length = maxWidgetRows;
|
|
506
|
-
out.push(`${theme.fg("dim", indent)}${theme.fg("dim", `↓
|
|
506
|
+
out.push(`${theme.fg("dim", indent)}${theme.fg("dim", `↓ ${overflow} lines hidden due to terminal height`)}`);
|
|
507
507
|
}
|
|
508
508
|
cachedOutput = out;
|
|
509
509
|
return cachedOutput;
|
|
@@ -571,7 +571,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
571
571
|
|
|
572
572
|
// Ctrl+Alt+J:收起态=选更早问题;展开态=回答下滚
|
|
573
573
|
pi.registerShortcut("ctrl+alt+j", {
|
|
574
|
-
description: "
|
|
574
|
+
description: "Current Questions: select an older question when collapsed / scroll the answer down when expanded",
|
|
575
575
|
handler: async (ctx) => {
|
|
576
576
|
if (recentQuestions.length === 0 || !enabled) return;
|
|
577
577
|
if (expanded) {
|
|
@@ -592,7 +592,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
592
592
|
|
|
593
593
|
// Ctrl+Alt+K:收起态=选更新问题;展开态=回答上滚
|
|
594
594
|
pi.registerShortcut("ctrl+alt+k", {
|
|
595
|
-
description: "
|
|
595
|
+
description: "Current Questions: select a newer question when collapsed / scroll the answer up when expanded",
|
|
596
596
|
handler: async (ctx) => {
|
|
597
597
|
if (recentQuestions.length === 0 || !enabled) return;
|
|
598
598
|
if (expanded) {
|
|
@@ -611,7 +611,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
611
611
|
|
|
612
612
|
// Ctrl+Alt+C:同时复制当前选中条的完整问题与回答
|
|
613
613
|
pi.registerShortcut("ctrl+alt+b", {
|
|
614
|
-
description: "
|
|
614
|
+
description: "Current Questions: copy the selected question and answer",
|
|
615
615
|
handler: async (ctx) => {
|
|
616
616
|
await copyQuestionAndAnswer(ctx);
|
|
617
617
|
},
|
|
@@ -619,7 +619,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
619
619
|
|
|
620
620
|
// Ctrl+Alt+L:隐藏态=重新显示;显示态=展开当前选中条(重置回答滚动到顶部)
|
|
621
621
|
pi.registerShortcut("ctrl+alt+l", {
|
|
622
|
-
description: "
|
|
622
|
+
description: "Current Questions: show when hidden / expand the selected question and answer when visible",
|
|
623
623
|
handler: async (ctx) => {
|
|
624
624
|
if (recentQuestions.length === 0) return;
|
|
625
625
|
if (!enabled) {
|
|
@@ -637,7 +637,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
637
637
|
|
|
638
638
|
// Ctrl+Alt+H:展开态=收起当前选中条;收起态=隐藏当前问题
|
|
639
639
|
pi.registerShortcut("ctrl+alt+h", {
|
|
640
|
-
description: "
|
|
640
|
+
description: "Current Questions: collapse the selected item when expanded / hide the widget when collapsed",
|
|
641
641
|
handler: async (ctx) => {
|
|
642
642
|
if (expanded) {
|
|
643
643
|
expanded = false;
|
|
@@ -654,20 +654,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
654
654
|
// 终端管理复用 pi 内置 openExternalEditor 的方式:tui.stop() 释放终端、异步 spawn 编辑器、
|
|
655
655
|
// 退出后 tui.start() 恢复。用异步 spawn 而非 spawnSync,避免 Windows 上 console input 抢占。
|
|
656
656
|
pi.registerShortcut("ctrl+alt+g", {
|
|
657
|
-
description: "
|
|
657
|
+
description: "Current Questions: open an external editor with selected question-and-answer context",
|
|
658
658
|
handler: async (ctx) => {
|
|
659
659
|
if (!enabled || recentQuestions.length === 0) {
|
|
660
|
-
ctx.ui.notify("
|
|
660
|
+
ctx.ui.notify("No question history available for context-aware editing", "warning");
|
|
661
661
|
return;
|
|
662
662
|
}
|
|
663
663
|
const q = getSelectedQuestion();
|
|
664
664
|
if (!q) {
|
|
665
|
-
ctx.ui.notify("
|
|
665
|
+
ctx.ui.notify("No history item is selected", "warning");
|
|
666
666
|
return;
|
|
667
667
|
}
|
|
668
668
|
const tui = tuiRef;
|
|
669
669
|
if (!tui?.stop || !tui?.start) {
|
|
670
|
-
ctx.ui.notify("
|
|
670
|
+
ctx.ui.notify("External editor workflow is unavailable in this environment", "error");
|
|
671
671
|
return;
|
|
672
672
|
}
|
|
673
673
|
|
|
@@ -686,7 +686,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
686
686
|
try {
|
|
687
687
|
fs.writeFileSync(tmpFile, fileContent, "utf8");
|
|
688
688
|
} catch (e) {
|
|
689
|
-
ctx.ui.notify(
|
|
689
|
+
ctx.ui.notify(`Failed to write temporary file: ${e instanceof Error ? e.message : String(e)}`, "error");
|
|
690
690
|
return;
|
|
691
691
|
}
|
|
692
692
|
|
|
@@ -711,7 +711,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
711
711
|
}
|
|
712
712
|
|
|
713
713
|
// stop 前直接写 stdout 提示(TUI 停止后 notify 不可见)
|
|
714
|
-
process.stdout.write(
|
|
714
|
+
process.stdout.write(`Opening ${command} to edit the reply with context from round ${q.round}…\nPi will resume when the editor exits.\n`);
|
|
715
715
|
|
|
716
716
|
let status: number | null = null;
|
|
717
717
|
let spawnErr: Error | null = null;
|
|
@@ -734,13 +734,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
734
734
|
}
|
|
735
735
|
|
|
736
736
|
if (spawnErr) {
|
|
737
|
-
ctx.ui.notify(
|
|
737
|
+
ctx.ui.notify(`Failed to launch editor: ${spawnErr.message}`, "error");
|
|
738
738
|
cleanupTemp();
|
|
739
739
|
return;
|
|
740
740
|
}
|
|
741
741
|
if (status !== 0) {
|
|
742
742
|
// 非正常退出:保留原输入框内容不变
|
|
743
|
-
ctx.ui.notify(
|
|
743
|
+
ctx.ui.notify(`Editor exited with code ${status}; editor contents were not changed`, "warning");
|
|
744
744
|
cleanupTemp();
|
|
745
745
|
return;
|
|
746
746
|
}
|
|
@@ -750,7 +750,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
750
750
|
try {
|
|
751
751
|
newContent = fs.readFileSync(tmpFile, "utf8");
|
|
752
752
|
} catch (e) {
|
|
753
|
-
ctx.ui.notify(
|
|
753
|
+
ctx.ui.notify(`Failed to read temporary file: ${e instanceof Error ? e.message : String(e)}`, "error");
|
|
754
754
|
cleanupTemp();
|
|
755
755
|
return;
|
|
756
756
|
}
|
|
@@ -759,16 +759,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
759
759
|
const { reply, markerFound } = parseReplyFromContent(newContent);
|
|
760
760
|
ctx.ui.setEditorText(reply);
|
|
761
761
|
if (!markerFound) {
|
|
762
|
-
ctx.ui.notify("⚠️
|
|
762
|
+
ctx.ui.notify("⚠️ Could not find the '>> Reply' marker; used the entire file as the reply", "warning");
|
|
763
763
|
} else {
|
|
764
|
-
ctx.ui.notify(
|
|
764
|
+
ctx.ui.notify(`Updated the editor with context from round ${q.round}`, "info");
|
|
765
765
|
}
|
|
766
766
|
},
|
|
767
767
|
});
|
|
768
768
|
|
|
769
769
|
// /current-question [on|off|toggle|<数字>]
|
|
770
770
|
pi.registerCommand("current-question", {
|
|
771
|
-
description: "
|
|
771
|
+
description: "Current Questions: use on/off/toggle to change visibility, or a number to set the displayed count (default: 5)",
|
|
772
772
|
handler: async (args, ctx) => {
|
|
773
773
|
const arg = args.trim().toLowerCase();
|
|
774
774
|
if (arg === "on") {
|
|
@@ -783,11 +783,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
783
783
|
enabled = true;
|
|
784
784
|
ensureVisible();
|
|
785
785
|
} else {
|
|
786
|
-
ctx.ui.notify("
|
|
786
|
+
ctx.ui.notify("Usage: /current-question [on|off|toggle|<number>]", "warning");
|
|
787
787
|
return;
|
|
788
788
|
}
|
|
789
789
|
refresh(ctx);
|
|
790
|
-
ctx.ui.notify(
|
|
790
|
+
ctx.ui.notify(`Current Questions: ${enabled ? "on" : "off"}; displaying ${maxLines} item(s)`, "info");
|
|
791
791
|
},
|
|
792
792
|
});
|
|
793
793
|
}
|
package/package.json
CHANGED