@rol1n/pi-kit 0.0.0-stage → 0.1.1

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.
Files changed (120) hide show
  1. package/README.md +73 -2
  2. package/ask-u-questions/README.md +26 -0
  3. package/ask-u-questions/dialog-ui.ts +80 -0
  4. package/ask-u-questions/index.ts +92 -0
  5. package/ask-u-questions/multi-choice.ts +258 -0
  6. package/ask-u-questions/single-choice.ts +180 -0
  7. package/ask-u-questions/tool-renderers.ts +67 -0
  8. package/ask-u-questions/types.ts +192 -0
  9. package/continue-signal/README.md +20 -0
  10. package/continue-signal/index.ts +69 -0
  11. package/custom-providers/README.md +20 -0
  12. package/custom-providers/asr/handlers/dashscope.ts +154 -0
  13. package/custom-providers/asr/index.ts +37 -0
  14. package/custom-providers/asr/models.ts +18 -0
  15. package/custom-providers/asr/types.ts +20 -0
  16. package/custom-providers/chat/index.ts +33 -0
  17. package/custom-providers/chat/magpie.ts +258 -0
  18. package/custom-providers/chat/models.ts +111 -0
  19. package/custom-providers/classifier/index.ts +6 -0
  20. package/custom-providers/classifier/models.ts +1 -0
  21. package/custom-providers/image/handlers/dashscope.ts +240 -0
  22. package/custom-providers/image/handlers/openrouter.ts +185 -0
  23. package/custom-providers/image/index.ts +96 -0
  24. package/custom-providers/image/models.ts +71 -0
  25. package/custom-providers/image/types.ts +22 -0
  26. package/custom-providers/index.ts +64 -0
  27. package/custom-providers/types.ts +17 -0
  28. package/image-generate/README.md +25 -0
  29. package/image-generate/files.ts +49 -0
  30. package/image-generate/index.ts +220 -0
  31. package/image-generate/model-selector.ts +150 -0
  32. package/memory-log/README.md +54 -0
  33. package/memory-log/agent/consolidator/prompt.ts +47 -0
  34. package/memory-log/agent/consolidator/tools.ts +160 -0
  35. package/memory-log/agent/cost.ts +32 -0
  36. package/memory-log/agent/index.ts +42 -0
  37. package/memory-log/agent/observer/prompt.ts +73 -0
  38. package/memory-log/agent/observer/tool.ts +73 -0
  39. package/memory-log/commands/compact.ts +35 -0
  40. package/memory-log/commands/consolidate.ts +47 -0
  41. package/memory-log/commands/status.ts +50 -0
  42. package/memory-log/config.ts +253 -0
  43. package/memory-log/debug-log.ts +76 -0
  44. package/memory-log/hooks/compaction-hook.ts +161 -0
  45. package/memory-log/hooks/compaction-trigger.ts +160 -0
  46. package/memory-log/hooks/consolidator-trigger.ts +143 -0
  47. package/memory-log/hooks/observer-trigger.ts +176 -0
  48. package/memory-log/ids.ts +78 -0
  49. package/memory-log/index.ts +149 -0
  50. package/memory-log/ledger/fold.ts +77 -0
  51. package/memory-log/ledger/index.ts +7 -0
  52. package/memory-log/ledger/pool.ts +38 -0
  53. package/memory-log/ledger/progress.ts +181 -0
  54. package/memory-log/ledger/projection.ts +115 -0
  55. package/memory-log/ledger/render.ts +38 -0
  56. package/memory-log/ledger/serialize.ts +161 -0
  57. package/memory-log/ledger/types.ts +201 -0
  58. package/memory-log/memory/index-render.ts +45 -0
  59. package/memory-log/memory/paths.ts +120 -0
  60. package/memory-log/memory/session.ts +64 -0
  61. package/memory-log/runtime.ts +127 -0
  62. package/memory-log/spawn/launch.ts +133 -0
  63. package/memory-log/spawn/runs.ts +83 -0
  64. package/memory-log/tokens.ts +66 -0
  65. package/memory-log/ui/status-controller.ts +225 -0
  66. package/memory-log/ui/timeline.ts +103 -0
  67. package/model-vision/README.md +22 -0
  68. package/multi-harness/README.md +63 -0
  69. package/multi-harness/adapters.mjs +72 -0
  70. package/multi-harness/harness-done.mjs +14 -0
  71. package/multi-harness/index.ts +375 -0
  72. package/multi-harness/ipc.mjs +37 -0
  73. package/multi-harness/opencode-plugin.mjs +99 -0
  74. package/multi-harness/process-runner.mjs +115 -0
  75. package/multi-harness/turn-hook.mjs +29 -0
  76. package/multi-harness/worker-bridge.ts +52 -0
  77. package/oai-subquota/README.md +22 -0
  78. package/oai-subquota/index.ts +286 -0
  79. package/oai-subquota/snapshot.ts +293 -0
  80. package/oai-subquota/ui.ts +183 -0
  81. package/package.json +92 -4
  82. package/prompt-snipaste/README.md +27 -0
  83. package/prompt-snipaste/index.ts +143 -0
  84. package/prompt-snipaste/menu.ts +159 -0
  85. package/prompt-snipaste/prompts/ask-questions.md +8 -0
  86. package/prompt-snipaste/prompts/delegate-exploration.md +7 -0
  87. package/prompt-snipaste/prompts/diagnose-report.md +7 -0
  88. package/prompt-snipaste/prompts/frontend-tech-preset.md +18 -0
  89. package/prompt-snipaste/prompts/orchestrator-mode.md +7 -0
  90. package/prompt-snipaste/prompts/session-kickoff.md +7 -0
  91. package/prompt-snipaste/prompts/verify-not-assume.md +7 -0
  92. package/prompt-snipaste/renderers.ts +87 -0
  93. package/prompt-snipaste/snippets.ts +206 -0
  94. package/prompt-snipaste/types.ts +18 -0
  95. package/screenshot/README.md +14 -0
  96. package/srp-theme/README.md +18 -0
  97. package/srp-theme/footer.ts +193 -0
  98. package/srp-theme/index.ts +172 -0
  99. package/srp-theme/tps-meter.ts +441 -0
  100. package/subagent/README.md +35 -0
  101. package/subagent/bridge.ts +171 -0
  102. package/subagent/index.ts +590 -0
  103. package/subagent/mux.ts +621 -0
  104. package/subagent/session.ts +166 -0
  105. package/subagent/ui.ts +100 -0
  106. package/tui-asr/README.md +21 -0
  107. package/tui-asr/dictation.ts +272 -0
  108. package/tui-asr/index.ts +181 -0
  109. package/tui-asr/input.ts +87 -0
  110. package/tui-asr/model-selector.ts +163 -0
  111. package/tui-asr/status.ts +79 -0
  112. package/web-reach/README.md +19 -0
  113. package/web-reach/html.ts +83 -0
  114. package/web-reach/index.ts +82 -0
  115. package/web-reach/page.ts +214 -0
  116. package/web-reach/rsc.ts +161 -0
  117. package/web-reach/search.ts +85 -0
  118. package/web-reach/transport.ts +35 -0
  119. package/windows-wheel-fix/README.md +12 -0
  120. package/windows-wheel-fix/index.ts +44 -0
@@ -0,0 +1,180 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Editor, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import { getOtherLabel, isChineseContext, type AskAnswer, type AskOption, type DisplayOption } from "./types.ts";
4
+ import { addWrapped, createEditorTheme } from "./dialog-ui.ts";
5
+
6
+ export async function askSingleChoice(
7
+ ctx: ExtensionContext,
8
+ question: string,
9
+ context: string | undefined,
10
+ options: AskOption[],
11
+ ): Promise<AskAnswer | null> {
12
+ const isZh = isChineseContext(question, options, context);
13
+ const otherLabel = getOtherLabel(options, isZh);
14
+ const allOptions: DisplayOption[] = [
15
+ ...options.map((option, index) => ({
16
+ ...option,
17
+ id: `option:${index}`,
18
+ index: index + 1,
19
+ })),
20
+ { id: "other", label: otherLabel, value: "__other__", isOther: true },
21
+ ];
22
+
23
+ return ctx.ui.custom<AskAnswer | null>(
24
+ (tui, theme, _kb, done: (result: AskAnswer | null) => void) => {
25
+ let optionIndex = 0;
26
+ let editMode = false;
27
+ let cachedLines: string[] | undefined;
28
+ let cachedWidth = -1;
29
+ let componentFocused = false;
30
+ const editor = new Editor(tui, createEditorTheme(theme));
31
+
32
+ editor.onSubmit = (value) => {
33
+ const trimmed = value.trim();
34
+ if (!trimmed) return;
35
+ done({ type: "other", label: trimmed, value: trimmed });
36
+ };
37
+
38
+ function refresh() {
39
+ cachedLines = undefined;
40
+ tui.requestRender();
41
+ }
42
+
43
+ function handleInput(data: string) {
44
+ if (editMode) {
45
+ if (matchesKey(data, Key.escape)) {
46
+ editMode = false;
47
+ editor.focused = false;
48
+ editor.setText("");
49
+ refresh();
50
+ return;
51
+ }
52
+ editor.handleInput(data);
53
+ refresh();
54
+ return;
55
+ }
56
+
57
+ if (matchesKey(data, Key.up)) {
58
+ optionIndex = Math.max(0, optionIndex - 1);
59
+ refresh();
60
+ return;
61
+ }
62
+ if (matchesKey(data, Key.down)) {
63
+ optionIndex = Math.min(allOptions.length - 1, optionIndex + 1);
64
+ refresh();
65
+ return;
66
+ }
67
+ if (matchesKey(data, Key.enter)) {
68
+ const selected = allOptions[optionIndex];
69
+ if (selected.isOther) {
70
+ editMode = true;
71
+ editor.focused = componentFocused;
72
+ editor.setText("");
73
+ refresh();
74
+ return;
75
+ }
76
+ done({
77
+ type: "option",
78
+ label: selected.label,
79
+ value: selected.value,
80
+ index: selected.index!,
81
+ });
82
+ return;
83
+ }
84
+ if (matchesKey(data, Key.escape)) {
85
+ done(null);
86
+ }
87
+ }
88
+
89
+ function render(width: number): string[] {
90
+ if (cachedLines && cachedWidth === width) return cachedLines;
91
+
92
+ const lines: string[] = [];
93
+ const add = (text: string) => lines.push(truncateToWidth(text, width));
94
+
95
+ add(theme.fg("accent", "─".repeat(width)));
96
+
97
+ if (editMode) {
98
+ addWrapped(lines, question, width, " ", " ", (l) => theme.fg("text", theme.bold(l)));
99
+ if (context) {
100
+ addWrapped(lines, context, width, " ", " ", (l) => theme.fg("muted", l));
101
+ }
102
+ lines.push("");
103
+ const header = allOptions[optionIndex]?.label || otherLabel;
104
+ add(theme.fg("accent", ` > ${header}:`));
105
+ for (const line of editor.render(Math.max(1, width - 4))) {
106
+ add(` ${line}`);
107
+ }
108
+ lines.push("");
109
+ const editHint = isZh
110
+ ? " Enter 提交 • Esc 返回选项"
111
+ : " Enter to submit • Esc to return";
112
+ add(theme.fg("dim", editHint));
113
+ add(theme.fg("accent", "─".repeat(width)));
114
+ cachedLines = lines;
115
+ cachedWidth = width;
116
+ return lines;
117
+ }
118
+
119
+ addWrapped(lines, question, width, " ", " ", (l) => theme.fg("text", theme.bold(l)));
120
+ if (context) {
121
+ addWrapped(lines, context, width, " ", " ", (l) => theme.fg("muted", l));
122
+ }
123
+ lines.push("");
124
+
125
+ for (let i = 0; i < allOptions.length; i++) {
126
+ const item = allOptions[i];
127
+ const selected = i === optionIndex;
128
+ const prefix = selected ? theme.fg("accent", " > ") : " ";
129
+
130
+ if (item.isOther) {
131
+ const styledLabel = selected
132
+ ? theme.bold(theme.fg("accent", item.label))
133
+ : theme.fg("text", item.label);
134
+ lines.push(truncateToWidth(`${prefix}${styledLabel}`, width));
135
+ } else {
136
+ const numPrefix = `${item.index}. `;
137
+ const firstIndent = `${prefix}${selected ? theme.fg("accent", numPrefix) : numPrefix}`;
138
+ const restIndent = " ".repeat(visibleWidth(firstIndent));
139
+ const styleLabel = (l: string) =>
140
+ selected ? theme.bold(theme.fg("accent", l)) : theme.fg("text", l);
141
+
142
+ addWrapped(lines, item.label, width, firstIndent, restIndent, styleLabel);
143
+
144
+ if (item.description) {
145
+ const descIndent = " ".repeat(visibleWidth(firstIndent));
146
+ const descStyle = (l: string) => theme.fg("muted", l);
147
+ addWrapped(lines, item.description, width, descIndent, descIndent, descStyle);
148
+ }
149
+ }
150
+ }
151
+
152
+ lines.push("");
153
+ const navHint = isZh
154
+ ? " ↑↓ 选择 • Enter 确定 • Esc 取消"
155
+ : " ↑↓ to navigate • Enter to select • Esc to cancel";
156
+ add(theme.fg("dim", navHint));
157
+ add(theme.fg("accent", "─".repeat(width)));
158
+
159
+ cachedLines = lines;
160
+ cachedWidth = width;
161
+ return lines;
162
+ }
163
+
164
+ return {
165
+ render,
166
+ invalidate: () => {
167
+ cachedLines = undefined;
168
+ },
169
+ handleInput,
170
+ get focused(): boolean {
171
+ return componentFocused;
172
+ },
173
+ set focused(value: boolean) {
174
+ componentFocused = value;
175
+ editor.focused = editMode && componentFocused;
176
+ },
177
+ };
178
+ },
179
+ );
180
+ }
@@ -0,0 +1,67 @@
1
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+ import { AskUserQuestionParams, getOtherLabel, isChineseContext, normalizeOptions, type AskUserQuestionResultDetails } from "./types.ts";
4
+
5
+ export const questionRenderers: Pick<ToolDefinition<typeof AskUserQuestionParams, AskUserQuestionResultDetails>, "renderCall" | "renderResult"> = {
6
+ renderCall(args, theme, context) {
7
+ const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
8
+ const options = normalizeOptions(
9
+ args.options as Array<{ label: string; value?: string; description?: string }> | undefined,
10
+ );
11
+ const isZh = isChineseContext(args.question || "", options, args.details);
12
+ let summary =
13
+ theme.fg("toolTitle", theme.bold("ask_user_question ")) +
14
+ theme.fg("accent", `"${args.question || ""}"`);
15
+ if (args.multiSelect) {
16
+ summary += theme.fg("dim", " [multi-select]");
17
+ }
18
+ if (options.length > 0) {
19
+ const otherLbl = getOtherLabel(options, isZh);
20
+ const labels = [...options.map((option) => option.label), otherLbl].join(", ");
21
+ const optPrefix = isZh ? " 选项: " : " Options: ";
22
+ summary += `\n${theme.fg("dim", `${optPrefix}${labels}`)}`;
23
+ }
24
+ text.setText(summary);
25
+ return text;
26
+ },
27
+
28
+ renderResult(result, _options, theme, context) {
29
+ const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
30
+ const details = result.details as AskUserQuestionResultDetails | undefined;
31
+ if (!details) {
32
+ const first = result.content?.[0];
33
+ text.setText(first?.type === "text" ? first.text : "");
34
+ return text;
35
+ }
36
+
37
+ const isZh = isChineseContext(details.question || "", details.answers, details.context);
38
+
39
+ if (details.status === "cancelled") {
40
+ const defaultCancelled = isZh ? "已取消" : "Cancelled";
41
+ text.setText(theme.fg("warning", `⊘ ${details.message || defaultCancelled}`));
42
+ return text;
43
+ }
44
+
45
+ if (details.status === "unavailable") {
46
+ const defaultUnavailable = isZh ? "提问工具不可用" : "Tool unavailable";
47
+ text.setText(theme.fg("error", `! ${details.message || defaultUnavailable}`));
48
+ return text;
49
+ }
50
+
51
+ const emptyText = isZh ? "(空回答)" : "(empty)";
52
+ const otherPrefix = isZh ? "其他:" : "Other: ";
53
+
54
+ const lines = details.answers.map((answer) => {
55
+ switch (answer.type) {
56
+ case "text":
57
+ return `${theme.fg("success", "✓ ")}${theme.fg("accent", answer.label || emptyText)}`;
58
+ case "other":
59
+ return `${theme.fg("success", "✓ ")}${theme.fg("muted", otherPrefix)}${theme.fg("accent", answer.label)}`;
60
+ case "option":
61
+ return `${theme.fg("success", "✓ ")}${theme.fg("accent", `${answer.index}. ${answer.label}`)}`;
62
+ }
63
+ });
64
+ text.setText(lines.join("\n"));
65
+ return text;
66
+ },
67
+ };
@@ -0,0 +1,192 @@
1
+ import { Type } from "typebox";
2
+
3
+ const OptionSchema = Type.Object({
4
+ label: Type.String({ description: 'Option label. Put a recommended option first and append "(Recommended)".' }),
5
+ value: Type.Optional(Type.String({ description: "Machine-readable value; defaults to the label." })),
6
+ description: Type.Optional(Type.String({ description: "Additional context shown below the option." })),
7
+ });
8
+
9
+ export const AskUserQuestionParams = Type.Object({
10
+ question: Type.String({ description: "The question to ask. Ask only one question per call." }),
11
+ details: Type.Optional(Type.String({ description: "Background or instructions shown below the question." })),
12
+ options: Type.Optional(Type.Array(OptionSchema, {
13
+ description: "Choices for selection. Omit or pass an empty array for free-text input. An Other choice is always available.",
14
+ })),
15
+ multiSelect: Type.Optional(Type.Boolean({ description: "Allow selecting multiple options when true." })),
16
+ });
17
+
18
+ export interface AskOption {
19
+ label: string;
20
+ value: string;
21
+ description?: string;
22
+ }
23
+
24
+ export interface DisplayOption extends AskOption {
25
+ id: string;
26
+ index?: number;
27
+ isOther?: boolean;
28
+ isSubmit?: boolean;
29
+ }
30
+
31
+ export interface TextAnswer {
32
+ type: "text";
33
+ label: string;
34
+ value: string;
35
+ }
36
+
37
+ export interface OptionAnswer {
38
+ type: "option";
39
+ label: string;
40
+ value: string;
41
+ index: number;
42
+ }
43
+
44
+ export interface OtherAnswer {
45
+ type: "other";
46
+ label: string;
47
+ value: string;
48
+ }
49
+
50
+ export type AskAnswer = TextAnswer | OptionAnswer | OtherAnswer;
51
+ export type AskUserQuestionStatus = "answered" | "cancelled" | "unavailable";
52
+ export type AskUserQuestionMode = "text" | "single-select" | "multi-select";
53
+
54
+ export interface AskUserQuestionResultDetails {
55
+ status: AskUserQuestionStatus;
56
+ question: string;
57
+ context?: string;
58
+ mode: AskUserQuestionMode;
59
+ answers: AskAnswer[];
60
+ message?: string;
61
+ }
62
+
63
+ export function normalizeOptions(
64
+ options: Array<{ label: string; value?: string; description?: string }> | undefined,
65
+ ): AskOption[] {
66
+ return (options || [])
67
+ .map((option) => ({
68
+ label: option.label.trim(),
69
+ value: option.value?.trim() || option.label.trim(),
70
+ description: option.description?.trim() || undefined,
71
+ }))
72
+ .filter((option) => option.label.length > 0);
73
+ }
74
+
75
+ export function isChineseContext(
76
+ question: string,
77
+ options?: Array<{ label?: string; description?: string }>,
78
+ context?: string,
79
+ ): boolean {
80
+ if (/[\u4e00-\u9fa5]/.test(question)) return true;
81
+ if (context && /[\u4e00-\u9fa5]/.test(context)) return true;
82
+ if (options && options.some((o) => (o.label && /[\u4e00-\u9fa5]/.test(o.label)) || (o.description && /[\u4e00-\u9fa5]/.test(o.description)))) {
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+
88
+ export function getOtherLabel(options: AskOption[], isZh = false): string {
89
+ if (isZh) {
90
+ return options.some((option) => option.label === "其他(自定义输入)")
91
+ ? "自定义输入"
92
+ : "其他(自定义输入)";
93
+ }
94
+ return options.some((option) => option.label === "Other (custom input)")
95
+ ? "Custom input"
96
+ : "Other (custom input)";
97
+ }
98
+
99
+ function answerSortRank(answer: AskAnswer): number {
100
+ switch (answer.type) {
101
+ case "option":
102
+ return answer.index;
103
+ case "other":
104
+ return Number.MAX_SAFE_INTEGER - 1;
105
+ case "text":
106
+ return Number.MAX_SAFE_INTEGER;
107
+ }
108
+ }
109
+
110
+ export function sortAnswers(answers: AskAnswer[]): AskAnswer[] {
111
+ return [...answers].sort((a, b) => answerSortRank(a) - answerSortRank(b));
112
+ }
113
+
114
+ function formatAnswerForModel(answer: AskAnswer): string {
115
+ switch (answer.type) {
116
+ case "text":
117
+ return answer.label;
118
+ case "other":
119
+ return `Other: ${answer.label}`;
120
+ case "option":
121
+ return `${answer.index}. ${answer.label}`;
122
+ }
123
+ }
124
+
125
+ function buildStructuredResult(
126
+ status: AskUserQuestionStatus,
127
+ question: string,
128
+ mode: AskUserQuestionMode,
129
+ answers: AskAnswer[],
130
+ context?: string,
131
+ message?: string,
132
+ ): AskUserQuestionResultDetails {
133
+ return {
134
+ status,
135
+ question,
136
+ context,
137
+ mode,
138
+ answers,
139
+ message,
140
+ };
141
+ }
142
+
143
+ export function cancelledResult(question: string, mode: AskUserQuestionMode, context?: string) {
144
+ const isZh = isChineseContext(question, undefined, context);
145
+ const message = isZh ? "用户取消了本次提问" : "User cancelled the question.";
146
+ return {
147
+ content: [{ type: "text" as const, text: message }],
148
+ details: buildStructuredResult("cancelled", question, mode, [], context, message),
149
+ };
150
+ }
151
+
152
+ export function unavailableResult(question: string, mode: AskUserQuestionMode, message: string, context?: string) {
153
+ return {
154
+ content: [{ type: "text" as const, text: message }],
155
+ details: buildStructuredResult("unavailable", question, mode, [], context, message),
156
+ };
157
+ }
158
+
159
+ export function buildResult(
160
+ question: string,
161
+ context: string | undefined,
162
+ mode: AskUserQuestionMode,
163
+ answers: AskAnswer[],
164
+ ) {
165
+ const isZh = isChineseContext(question, answers, context);
166
+ let text: string;
167
+ if (mode === "text") {
168
+ const answer = answers[0];
169
+ if (isZh) {
170
+ text = answer.label.trim().length > 0
171
+ ? `用户输入回答: ${answer.label}`
172
+ : "用户提交了空回答";
173
+ } else {
174
+ text = answer.label.trim().length > 0
175
+ ? `User answered: ${answer.label}`
176
+ : "User submitted an empty answer";
177
+ }
178
+ } else if (mode === "single-select") {
179
+ text = isZh
180
+ ? `用户选择项: ${formatAnswerForModel(answers[0])}`
181
+ : `User selected: ${formatAnswerForModel(answers[0])}`;
182
+ } else {
183
+ text = isZh
184
+ ? `用户选择多项:\n${answers.map((answer) => `- ${formatAnswerForModel(answer)}`).join("\n")}`
185
+ : `User selected multiple:\n${answers.map((answer) => `- ${formatAnswerForModel(answer)}`).join("\n")}`;
186
+ }
187
+
188
+ return {
189
+ content: [{ type: "text" as const, text }],
190
+ details: buildStructuredResult("answered", question, mode, answers, context),
191
+ };
192
+ }
@@ -0,0 +1,20 @@
1
+ # continue-signal
2
+
3
+ Resume an idle Pi agent with a visible continuation signal.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pi install npm:@rol1n/continue-signal
9
+ ```
10
+
11
+ Also included in `@rol1n/pi-kit`.
12
+
13
+ ## Commands and shortcuts
14
+
15
+ | Action | Trigger |
16
+ |---|---|
17
+ | Resume unfinished work | `/continue` or `Alt+C` |
18
+ | Resume with custom instruction | `/continue Review the remaining test failures` |
19
+
20
+ An explicit command argument takes priority; otherwise the current editor draft is consumed, falling back to a default resume prompt.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * continue-signal — Resume an idle agent via /continue [prompt] or Alt+C.
3
+ * Uses the editor draft when no prompt is supplied.
4
+ */
5
+
6
+ import type { ExtensionAPI, ExtensionContext, MessageRenderer } from "@earendil-works/pi-coding-agent";
7
+ import { Box, Key, Text } from "@earendil-works/pi-tui";
8
+
9
+ const DEFAULT_PROMPT = "Review the conversation context and resume any unfinished work from where it stopped.";
10
+ const CUSTOM_TYPE = "continue-signal";
11
+
12
+ const renderContinueSignal: MessageRenderer<{ timestamp?: number }> = (
13
+ message,
14
+ { expanded, outputPad },
15
+ theme,
16
+ ) => {
17
+ const time = new Date(message.details?.timestamp ?? Date.now()).toLocaleTimeString();
18
+ const title = `${theme.bold(theme.fg("accent", "⚡ LOOP RESUME SIGNAL"))} ${theme.fg("muted", `[${time}]` )}`;
19
+
20
+ const box = new Box(outputPad, 1, (text) => theme.bg("customMessageBg", text));
21
+ box.addChild(new Text(title, 0, 0));
22
+ box.addChild(new Text(theme.fg("text", "Re-linking conversation context & resuming agent loop..."), 0, 0));
23
+
24
+ if (expanded && message.content) {
25
+ box.addChild(new Text(theme.fg("dim", `Prompt: ${message.content}`), 0, 0));
26
+ }
27
+
28
+ return box;
29
+ };
30
+
31
+ export default function (pi: ExtensionAPI) {
32
+ pi.registerMessageRenderer(CUSTOM_TYPE, renderContinueSignal);
33
+
34
+ const triggerContinue = (ctx: ExtensionContext, customPrompt = "") => {
35
+ if (!ctx.isIdle()) {
36
+ ctx.ui.notify("Agent is currently running; no need to wake up.", "warning");
37
+ return;
38
+ }
39
+
40
+ let prompt = customPrompt.trim();
41
+ if (!prompt) {
42
+ const draft = ctx.ui.getEditorText?.()?.trim();
43
+ if (draft) {
44
+ prompt = draft;
45
+ ctx.ui.setEditorText?.("");
46
+ }
47
+ }
48
+
49
+ pi.sendMessage(
50
+ {
51
+ customType: CUSTOM_TYPE,
52
+ content: prompt || DEFAULT_PROMPT,
53
+ display: true,
54
+ details: { timestamp: Date.now() },
55
+ },
56
+ { triggerTurn: true, deliverAs: "steer" },
57
+ );
58
+ };
59
+
60
+ pi.registerCommand("continue", {
61
+ description: "Send a resume signal to continue unfinished work (optional prompt)",
62
+ handler: (args, ctx) => triggerContinue(ctx, args),
63
+ });
64
+
65
+ pi.registerShortcut(Key.alt("c"), {
66
+ description: "Resume unfinished work",
67
+ handler: (ctx) => triggerContinue(ctx),
68
+ });
69
+ }
@@ -0,0 +1,20 @@
1
+ # custom-providers
2
+
3
+ Registers additional model providers through Pi's native model registry and `/login` interface.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pi install npm:@rol1n/custom-providers
9
+ ```
10
+
11
+ Also included in `@rol1n/pi-kit`.
12
+
13
+ ## Providers
14
+
15
+ | Provider | Configuration |
16
+ |---|---|
17
+ | Magpie | `MAGPIE_BASE_URL` (default `http://127.0.0.1:3425/v1`); authenticate via `MAGPIE_GATEWAY_KEY` or `/login magpie` |
18
+ | SHUAI API | Authenticate via `/login shuaiapi` |
19
+ | BigModel | Authenticate via `/login bigmodel` |
20
+ | DashScope | Authenticate via `/login dashscope`; `DASHSCOPE_IMAGE_URL` overrides the image API base URL |