@rol1n/pi-kit 0.0.0-stage → 0.1.0

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 (136) hide show
  1. package/README.md +98 -2
  2. package/extensions/ask-u-questions/README.md +32 -0
  3. package/extensions/ask-u-questions/answers.ts +53 -0
  4. package/extensions/ask-u-questions/dialog-ui.ts +57 -0
  5. package/extensions/ask-u-questions/index.ts +88 -0
  6. package/extensions/ask-u-questions/multi-choice.ts +260 -0
  7. package/extensions/ask-u-questions/results.ts +83 -0
  8. package/extensions/ask-u-questions/schema.ts +17 -0
  9. package/extensions/ask-u-questions/single-choice.ts +182 -0
  10. package/extensions/ask-u-questions/tool-renderers.ts +69 -0
  11. package/extensions/ask-u-questions/types.ts +45 -0
  12. package/extensions/ask-u-questions/ui-lock.ts +24 -0
  13. package/extensions/continue-signal/README.md +28 -0
  14. package/extensions/continue-signal/index.ts +51 -0
  15. package/extensions/continue-signal/renderer.ts +21 -0
  16. package/extensions/custom-providers/README.md +28 -0
  17. package/extensions/custom-providers/asr/handlers/dashscope.ts +154 -0
  18. package/extensions/custom-providers/asr/index.ts +37 -0
  19. package/extensions/custom-providers/asr/models.ts +18 -0
  20. package/extensions/custom-providers/asr/types.ts +20 -0
  21. package/extensions/custom-providers/chat/index.ts +33 -0
  22. package/extensions/custom-providers/chat/magpie.ts +258 -0
  23. package/extensions/custom-providers/chat/models.ts +111 -0
  24. package/extensions/custom-providers/classifier/index.ts +6 -0
  25. package/extensions/custom-providers/classifier/models.ts +1 -0
  26. package/extensions/custom-providers/image/handlers/dashscope.ts +240 -0
  27. package/extensions/custom-providers/image/handlers/openrouter.ts +185 -0
  28. package/extensions/custom-providers/image/index.ts +96 -0
  29. package/extensions/custom-providers/image/models.ts +71 -0
  30. package/extensions/custom-providers/image/types.ts +22 -0
  31. package/extensions/custom-providers/index.ts +50 -0
  32. package/extensions/custom-providers/types.ts +17 -0
  33. package/extensions/image-generate/README.md +29 -0
  34. package/extensions/image-generate/files.ts +49 -0
  35. package/extensions/image-generate/index.ts +200 -0
  36. package/extensions/image-generate/model-selector.ts +150 -0
  37. package/extensions/image-generate/models.ts +19 -0
  38. package/extensions/memory-log/README.md +54 -0
  39. package/extensions/memory-log/agent/consolidator/prompt.ts +47 -0
  40. package/extensions/memory-log/agent/consolidator/tools.ts +160 -0
  41. package/extensions/memory-log/agent/cost.ts +32 -0
  42. package/extensions/memory-log/agent/index.ts +42 -0
  43. package/extensions/memory-log/agent/observer/prompt.ts +73 -0
  44. package/extensions/memory-log/agent/observer/tool.ts +73 -0
  45. package/extensions/memory-log/commands/compact.ts +35 -0
  46. package/extensions/memory-log/commands/consolidate.ts +47 -0
  47. package/extensions/memory-log/commands/status.ts +50 -0
  48. package/extensions/memory-log/config.ts +253 -0
  49. package/extensions/memory-log/debug-log.ts +76 -0
  50. package/extensions/memory-log/hooks/compaction-hook.ts +161 -0
  51. package/extensions/memory-log/hooks/compaction-trigger.ts +160 -0
  52. package/extensions/memory-log/hooks/consolidator-trigger.ts +143 -0
  53. package/extensions/memory-log/hooks/observer-trigger.ts +176 -0
  54. package/extensions/memory-log/ids.ts +78 -0
  55. package/extensions/memory-log/index.ts +149 -0
  56. package/extensions/memory-log/ledger/fold.ts +77 -0
  57. package/extensions/memory-log/ledger/index.ts +7 -0
  58. package/extensions/memory-log/ledger/pool.ts +38 -0
  59. package/extensions/memory-log/ledger/progress.ts +181 -0
  60. package/extensions/memory-log/ledger/projection.ts +115 -0
  61. package/extensions/memory-log/ledger/render.ts +38 -0
  62. package/extensions/memory-log/ledger/serialize.ts +161 -0
  63. package/extensions/memory-log/ledger/types.ts +201 -0
  64. package/extensions/memory-log/memory/index-render.ts +45 -0
  65. package/extensions/memory-log/memory/paths.ts +120 -0
  66. package/extensions/memory-log/memory/session.ts +64 -0
  67. package/extensions/memory-log/runtime.ts +127 -0
  68. package/extensions/memory-log/spawn/launch.ts +133 -0
  69. package/extensions/memory-log/spawn/runs.ts +83 -0
  70. package/extensions/memory-log/tokens.ts +66 -0
  71. package/extensions/memory-log/ui/status-controller.ts +225 -0
  72. package/extensions/memory-log/ui/timeline.ts +103 -0
  73. package/extensions/model-vision/README.md +28 -0
  74. package/extensions/multi-harness/README.md +63 -0
  75. package/extensions/multi-harness/adapters.mjs +72 -0
  76. package/extensions/multi-harness/harness-done.mjs +14 -0
  77. package/extensions/multi-harness/index.ts +375 -0
  78. package/extensions/multi-harness/ipc.mjs +37 -0
  79. package/extensions/multi-harness/opencode-plugin.mjs +99 -0
  80. package/extensions/multi-harness/process-runner.mjs +115 -0
  81. package/extensions/multi-harness/turn-hook.mjs +29 -0
  82. package/extensions/multi-harness/worker-bridge.ts +52 -0
  83. package/extensions/oai-subquota/README.md +30 -0
  84. package/extensions/oai-subquota/api.ts +27 -0
  85. package/extensions/oai-subquota/auth.ts +34 -0
  86. package/extensions/oai-subquota/cache.ts +30 -0
  87. package/extensions/oai-subquota/index.ts +276 -0
  88. package/extensions/oai-subquota/snapshot.ts +167 -0
  89. package/extensions/oai-subquota/types.ts +40 -0
  90. package/extensions/oai-subquota/ui.ts +183 -0
  91. package/extensions/prompt-snipaste/README.md +33 -0
  92. package/extensions/prompt-snipaste/index.ts +143 -0
  93. package/extensions/prompt-snipaste/menu.ts +159 -0
  94. package/extensions/prompt-snipaste/renderers.ts +88 -0
  95. package/extensions/prompt-snipaste/snippets.ts +203 -0
  96. package/extensions/prompt-snipaste/types.ts +19 -0
  97. package/extensions/screenshot/README.md +25 -0
  98. package/extensions/srp-theme/README.md +24 -0
  99. package/extensions/srp-theme/footer.ts +194 -0
  100. package/extensions/srp-theme/index.ts +172 -0
  101. package/extensions/srp-theme/tps-meter.ts +442 -0
  102. package/extensions/subagent/README.md +35 -0
  103. package/extensions/subagent/bridge.ts +171 -0
  104. package/extensions/subagent/index.ts +590 -0
  105. package/extensions/subagent/mux.ts +621 -0
  106. package/extensions/subagent/session.ts +166 -0
  107. package/extensions/subagent/skills/subagent/SKILL.md +70 -0
  108. package/extensions/subagent/ui.ts +100 -0
  109. package/extensions/tui-asr/README.md +28 -0
  110. package/extensions/tui-asr/audio.ts +40 -0
  111. package/extensions/tui-asr/clipboard.ts +30 -0
  112. package/extensions/tui-asr/dictation.ts +205 -0
  113. package/extensions/tui-asr/index.ts +181 -0
  114. package/extensions/tui-asr/input.ts +58 -0
  115. package/extensions/tui-asr/model-selector.ts +163 -0
  116. package/extensions/tui-asr/providers.ts +25 -0
  117. package/extensions/tui-asr/status.ts +79 -0
  118. package/extensions/web-reach/README.md +37 -0
  119. package/extensions/web-reach/html.ts +83 -0
  120. package/extensions/web-reach/index.ts +82 -0
  121. package/extensions/web-reach/page.ts +132 -0
  122. package/extensions/web-reach/pdf.ts +42 -0
  123. package/extensions/web-reach/reader.ts +42 -0
  124. package/extensions/web-reach/rsc.ts +161 -0
  125. package/extensions/web-reach/search.ts +85 -0
  126. package/extensions/web-reach/transport.ts +35 -0
  127. package/extensions/windows-wheel-fix/README.md +21 -0
  128. package/extensions/windows-wheel-fix/index.ts +44 -0
  129. package/package.json +93 -4
  130. package/prompts/ask-questions.md +8 -0
  131. package/prompts/delegate-exploration.md +7 -0
  132. package/prompts/diagnose-report.md +7 -0
  133. package/prompts/frontend-tech-preset.md +18 -0
  134. package/prompts/orchestrator-mode.md +7 -0
  135. package/prompts/session-kickoff.md +7 -0
  136. package/prompts/verify-not-assume.md +7 -0
@@ -0,0 +1,17 @@
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
+
@@ -0,0 +1,182 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Editor, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import type { AskAnswer, AskOption, DisplayOption } from "./types.ts";
4
+ import { getOtherLabel, isChineseContext } from "./answers.ts";
5
+ import { addWrapped, createEditorTheme } from "./dialog-ui.ts";
6
+
7
+ export async function askSingleChoice(
8
+ ctx: ExtensionContext,
9
+ question: string,
10
+ context: string | undefined,
11
+ options: AskOption[],
12
+ ): Promise<AskAnswer | null> {
13
+ const isZh = isChineseContext(question, options, context);
14
+ const otherLabel = getOtherLabel(options, isZh);
15
+ const allOptions: DisplayOption[] = [
16
+ ...options.map((option, index) => ({
17
+ ...option,
18
+ id: `option:${index}`,
19
+ index: index + 1,
20
+ })),
21
+ { id: "other", label: otherLabel, value: "__other__", isOther: true },
22
+ ];
23
+
24
+ return ctx.ui.custom<AskAnswer | null>(
25
+ (tui, theme, _kb, done: (result: AskAnswer | null) => void) => {
26
+ let optionIndex = 0;
27
+ let editMode = false;
28
+ let cachedLines: string[] | undefined;
29
+ let cachedWidth = -1;
30
+ let componentFocused = false;
31
+ const editor = new Editor(tui, createEditorTheme(theme));
32
+
33
+ editor.onSubmit = (value) => {
34
+ const trimmed = value.trim();
35
+ if (!trimmed) return;
36
+ done({ type: "other", label: trimmed, value: trimmed });
37
+ };
38
+
39
+ function refresh() {
40
+ cachedLines = undefined;
41
+ tui.requestRender();
42
+ }
43
+
44
+ function handleInput(data: string) {
45
+ if (editMode) {
46
+ if (matchesKey(data, Key.escape)) {
47
+ editMode = false;
48
+ editor.focused = false;
49
+ editor.setText("");
50
+ refresh();
51
+ return;
52
+ }
53
+ editor.handleInput(data);
54
+ refresh();
55
+ return;
56
+ }
57
+
58
+ if (matchesKey(data, Key.up)) {
59
+ optionIndex = Math.max(0, optionIndex - 1);
60
+ refresh();
61
+ return;
62
+ }
63
+ if (matchesKey(data, Key.down)) {
64
+ optionIndex = Math.min(allOptions.length - 1, optionIndex + 1);
65
+ refresh();
66
+ return;
67
+ }
68
+ if (matchesKey(data, Key.enter)) {
69
+ const selected = allOptions[optionIndex];
70
+ if (selected.isOther) {
71
+ editMode = true;
72
+ editor.focused = componentFocused;
73
+ editor.setText("");
74
+ refresh();
75
+ return;
76
+ }
77
+ done({
78
+ type: "option",
79
+ label: selected.label,
80
+ value: selected.value,
81
+ index: selected.index!,
82
+ });
83
+ return;
84
+ }
85
+ if (matchesKey(data, Key.escape)) {
86
+ done(null);
87
+ }
88
+ }
89
+
90
+ function render(width: number): string[] {
91
+ if (cachedLines && cachedWidth === width) return cachedLines;
92
+
93
+ const lines: string[] = [];
94
+ const add = (text: string) => lines.push(truncateToWidth(text, width));
95
+
96
+ add(theme.fg("accent", "─".repeat(width)));
97
+
98
+ if (editMode) {
99
+ addWrapped(lines, question, width, " ", " ", (l) => theme.fg("text", theme.bold(l)));
100
+ if (context) {
101
+ addWrapped(lines, context, width, " ", " ", (l) => theme.fg("muted", l));
102
+ }
103
+ lines.push("");
104
+ const header = allOptions[optionIndex]?.label || otherLabel;
105
+ add(theme.fg("accent", ` > ${header}:`));
106
+ for (const line of editor.render(Math.max(1, width - 4))) {
107
+ add(` ${line}`);
108
+ }
109
+ lines.push("");
110
+ const editHint = isZh
111
+ ? " Enter 提交 • Esc 返回选项"
112
+ : " Enter to submit • Esc to return";
113
+ add(theme.fg("dim", editHint));
114
+ add(theme.fg("accent", "─".repeat(width)));
115
+ cachedLines = lines;
116
+ cachedWidth = width;
117
+ return lines;
118
+ }
119
+
120
+ addWrapped(lines, question, width, " ", " ", (l) => theme.fg("text", theme.bold(l)));
121
+ if (context) {
122
+ addWrapped(lines, context, width, " ", " ", (l) => theme.fg("muted", l));
123
+ }
124
+ lines.push("");
125
+
126
+ for (let i = 0; i < allOptions.length; i++) {
127
+ const item = allOptions[i];
128
+ const selected = i === optionIndex;
129
+ const prefix = selected ? theme.fg("accent", " > ") : " ";
130
+
131
+ if (item.isOther) {
132
+ const styledLabel = selected
133
+ ? theme.bold(theme.fg("accent", item.label))
134
+ : theme.fg("text", item.label);
135
+ lines.push(truncateToWidth(`${prefix}${styledLabel}`, width));
136
+ } else {
137
+ const numPrefix = `${item.index}. `;
138
+ const firstIndent = `${prefix}${selected ? theme.fg("accent", numPrefix) : numPrefix}`;
139
+ const restIndent = " ".repeat(visibleWidth(firstIndent));
140
+ const styleLabel = (l: string) =>
141
+ selected ? theme.bold(theme.fg("accent", l)) : theme.fg("text", l);
142
+
143
+ addWrapped(lines, item.label, width, firstIndent, restIndent, styleLabel);
144
+
145
+ if (item.description) {
146
+ const descIndent = " ".repeat(visibleWidth(firstIndent));
147
+ const descStyle = (l: string) => theme.fg("muted", l);
148
+ addWrapped(lines, item.description, width, descIndent, descIndent, descStyle);
149
+ }
150
+ }
151
+ }
152
+
153
+ lines.push("");
154
+ const navHint = isZh
155
+ ? " ↑↓ 选择 • Enter 确定 • Esc 取消"
156
+ : " ↑↓ to navigate • Enter to select • Esc to cancel";
157
+ add(theme.fg("dim", navHint));
158
+ add(theme.fg("accent", "─".repeat(width)));
159
+
160
+ cachedLines = lines;
161
+ cachedWidth = width;
162
+ return lines;
163
+ }
164
+
165
+ return {
166
+ render,
167
+ invalidate: () => {
168
+ cachedLines = undefined;
169
+ },
170
+ handleInput,
171
+ get focused(): boolean {
172
+ return componentFocused;
173
+ },
174
+ set focused(value: boolean) {
175
+ componentFocused = value;
176
+ editor.focused = editMode && componentFocused;
177
+ },
178
+ };
179
+ },
180
+ );
181
+ }
182
+
@@ -0,0 +1,69 @@
1
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+ import type { AskUserQuestionParams } from "./schema.ts";
4
+ import type { AskUserQuestionResultDetails } from "./types.ts";
5
+ import { getOtherLabel, isChineseContext, normalizeOptions } from "./answers.ts";
6
+
7
+ export const questionRenderers: Pick<ToolDefinition<typeof AskUserQuestionParams, AskUserQuestionResultDetails>, "renderCall" | "renderResult"> = {
8
+ renderCall(args, theme, context) {
9
+ const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
10
+ const options = normalizeOptions(
11
+ args.options as Array<{ label: string; value?: string; description?: string }> | undefined,
12
+ );
13
+ const isZh = isChineseContext(args.question || "", options, args.details);
14
+ let summary =
15
+ theme.fg("toolTitle", theme.bold("ask_user_question ")) +
16
+ theme.fg("accent", `"${args.question || ""}"`);
17
+ if (args.multiSelect) {
18
+ summary += theme.fg("dim", " [multi-select]");
19
+ }
20
+ if (options.length > 0) {
21
+ const otherLbl = getOtherLabel(options, isZh);
22
+ const labels = [...options.map((option) => option.label), otherLbl].join(", ");
23
+ const optPrefix = isZh ? " 选项: " : " Options: ";
24
+ summary += `\n${theme.fg("dim", `${optPrefix}${labels}`)}`;
25
+ }
26
+ text.setText(summary);
27
+ return text;
28
+ },
29
+
30
+ renderResult(result, _options, theme, context) {
31
+ const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
32
+ const details = result.details as AskUserQuestionResultDetails | undefined;
33
+ if (!details) {
34
+ const first = result.content?.[0];
35
+ text.setText(first?.type === "text" ? first.text : "");
36
+ return text;
37
+ }
38
+
39
+ const isZh = isChineseContext(details.question || "", details.answers, details.context);
40
+
41
+ if (details.status === "cancelled") {
42
+ const defaultCancelled = isZh ? "已取消" : "Cancelled";
43
+ text.setText(theme.fg("warning", `⊘ ${details.message || defaultCancelled}`));
44
+ return text;
45
+ }
46
+
47
+ if (details.status === "unavailable") {
48
+ const defaultUnavailable = isZh ? "提问工具不可用" : "Tool unavailable";
49
+ text.setText(theme.fg("error", `! ${details.message || defaultUnavailable}`));
50
+ return text;
51
+ }
52
+
53
+ const emptyText = isZh ? "(空回答)" : "(empty)";
54
+ const otherPrefix = isZh ? "其他:" : "Other: ";
55
+
56
+ const lines = details.answers.map((answer) => {
57
+ switch (answer.type) {
58
+ case "text":
59
+ return `${theme.fg("success", "✓ ")}${theme.fg("accent", answer.label || emptyText)}`;
60
+ case "other":
61
+ return `${theme.fg("success", "✓ ")}${theme.fg("muted", otherPrefix)}${theme.fg("accent", answer.label)}`;
62
+ case "option":
63
+ return `${theme.fg("success", "✓ ")}${theme.fg("accent", `${answer.index}. ${answer.label}`)}`;
64
+ }
65
+ });
66
+ text.setText(lines.join("\n"));
67
+ return text;
68
+ },
69
+ };
@@ -0,0 +1,45 @@
1
+ export interface AskOption {
2
+ label: string;
3
+ value: string;
4
+ description?: string;
5
+ }
6
+
7
+ export interface DisplayOption extends AskOption {
8
+ id: string;
9
+ index?: number;
10
+ isOther?: boolean;
11
+ isSubmit?: boolean;
12
+ }
13
+
14
+ export interface TextAnswer {
15
+ type: "text";
16
+ label: string;
17
+ value: string;
18
+ }
19
+
20
+ export interface OptionAnswer {
21
+ type: "option";
22
+ label: string;
23
+ value: string;
24
+ index: number;
25
+ }
26
+
27
+ export interface OtherAnswer {
28
+ type: "other";
29
+ label: string;
30
+ value: string;
31
+ }
32
+
33
+ export type AskAnswer = TextAnswer | OptionAnswer | OtherAnswer;
34
+ export type AskUserQuestionStatus = "answered" | "cancelled" | "unavailable";
35
+ export type AskUserQuestionMode = "text" | "single-select" | "multi-select";
36
+
37
+ export interface AskUserQuestionResultDetails {
38
+ status: AskUserQuestionStatus;
39
+ question: string;
40
+ context?: string;
41
+ mode: AskUserQuestionMode;
42
+ answers: AskAnswer[];
43
+ message?: string;
44
+ }
45
+
@@ -0,0 +1,24 @@
1
+ const SHARED_UI_LOCK_KEY = "__piSharedUiLock";
2
+ function getSharedUiLock(): { withLock<T>(fn: () => T | Promise<T>): Promise<T> } {
3
+ const g = globalThis as any;
4
+ if (!g[SHARED_UI_LOCK_KEY]) {
5
+ let chain: Promise<void> = Promise.resolve();
6
+ g[SHARED_UI_LOCK_KEY] = {
7
+ withLock<T>(fn: () => T | Promise<T>): Promise<T> {
8
+ const prev = chain;
9
+ let release: () => void;
10
+ chain = new Promise<void>((r) => {
11
+ release = r;
12
+ });
13
+ return prev.then(fn).finally(() => release!());
14
+ },
15
+ };
16
+ }
17
+ return g[SHARED_UI_LOCK_KEY];
18
+ }
19
+ const sharedUiLock = getSharedUiLock();
20
+
21
+ export function withUILock<T>(fn: () => Promise<T>): Promise<T> {
22
+ return sharedUiLock.withLock(fn);
23
+ }
24
+
@@ -0,0 +1,28 @@
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
14
+
15
+ | Action | Command or shortcut |
16
+ |---|---|
17
+ | Resume unfinished work | `/continue` or Alt+C |
18
+ | Resume with a specific instruction | `/continue Review the remaining test failures` |
19
+
20
+ An explicit command argument takes priority. Without one, the extension uses and clears the current editor draft. If the draft is empty, it asks the agent to review the conversation and resume unfinished work. It leaves an already-running agent alone.
21
+
22
+ ## Configuration
23
+
24
+ No settings or credentials are needed. The shortcut requires a terminal that forwards Alt+C. This extension starts a new agent turn; it does not reconstruct a lost session or restart a crashed process.
25
+
26
+ ## Source layout
27
+
28
+ `index.ts` registers the command and shortcut and sends continuation messages. `renderer.ts` owns the visible signal card.
@@ -0,0 +1,51 @@
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 } from "@earendil-works/pi-coding-agent";
7
+ import { Key } from "@earendil-works/pi-tui";
8
+ import { renderContinueSignal } from "./renderer.ts";
9
+
10
+ const DEFAULT_PROMPT = "Review the conversation context and resume any unfinished work from where it stopped.";
11
+ const CUSTOM_TYPE = "continue-signal";
12
+
13
+ export default function (pi: ExtensionAPI) {
14
+ pi.registerMessageRenderer(CUSTOM_TYPE, renderContinueSignal);
15
+
16
+ const triggerContinue = (ctx: ExtensionContext, customPrompt = "") => {
17
+ if (!ctx.isIdle()) {
18
+ ctx.ui.notify("Agent is currently running; no need to wake up.", "warning");
19
+ return;
20
+ }
21
+
22
+ let prompt = customPrompt.trim();
23
+ if (!prompt) {
24
+ const draft = ctx.ui.getEditorText?.()?.trim();
25
+ if (draft) {
26
+ prompt = draft;
27
+ ctx.ui.setEditorText?.("");
28
+ }
29
+ }
30
+
31
+ pi.sendMessage(
32
+ {
33
+ customType: CUSTOM_TYPE,
34
+ content: prompt || DEFAULT_PROMPT,
35
+ display: true,
36
+ details: { timestamp: Date.now() },
37
+ },
38
+ { triggerTurn: true, deliverAs: "steer" },
39
+ );
40
+ };
41
+
42
+ pi.registerCommand("continue", {
43
+ description: "Send a resume signal to continue unfinished work (optional prompt)",
44
+ handler: (args, ctx) => triggerContinue(ctx, args),
45
+ });
46
+
47
+ pi.registerShortcut(Key.alt("c"), {
48
+ description: "Resume unfinished work",
49
+ handler: (ctx) => triggerContinue(ctx),
50
+ });
51
+ }
@@ -0,0 +1,21 @@
1
+ import type { MessageRenderer } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Text } from "@earendil-works/pi-tui";
3
+
4
+ export const renderContinueSignal: MessageRenderer<{ timestamp?: number }> = (
5
+ message,
6
+ { expanded, outputPad },
7
+ theme,
8
+ ) => {
9
+ const time = new Date(message.details?.timestamp ?? Date.now()).toLocaleTimeString();
10
+ const title = `${theme.bold(theme.fg("accent", "⚡ LOOP RESUME SIGNAL"))} ${theme.fg("muted", `[${time}]`)}`;
11
+
12
+ const box = new Box(outputPad, 1, (text) => theme.bg("customMessageBg", text));
13
+ box.addChild(new Text(title, 0, 0));
14
+ box.addChild(new Text(theme.fg("text", "Re-linking conversation context & resuming agent loop..."), 0, 0));
15
+
16
+ if (expanded && message.content) {
17
+ box.addChild(new Text(theme.fg("dim", `Prompt: ${message.content}`), 0, 0));
18
+ }
19
+
20
+ return box;
21
+ };
@@ -0,0 +1,28 @@
1
+ # custom-providers
2
+
3
+ Register additional model providers through Pi's native model registry and login interface. Chat, image, classifier, and ASR definitions are organized by model type.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pi install npm:@rol1n/custom-providers
9
+ ```
10
+
11
+ Also included in `@rol1n/pi-kit`. Open Pi's model selector after provider registration and authenticate only the providers you use.
12
+
13
+ ## Provider configuration
14
+
15
+ | Provider | Configuration |
16
+ |---|---|
17
+ | Magpie | `MAGPIE_BASE_URL` selects the gateway; `MAGPIE_GATEWAY_KEY` or `/login magpie` supplies its gateway key |
18
+ | SHUAI API | `/login shuaiapi`; chat catalog and endpoint are declared in `chat/` |
19
+ | BigModel | `/login bigmodel`; chat catalog and endpoint are declared in `chat/` |
20
+ | DashScope | `/login dashscope`; custom chat/image definitions and ASR model helpers |
21
+
22
+ Magpie defaults to `http://127.0.0.1:3425/v1`. Set `MAGPIE_BASE_URL` before starting Pi when your gateway runs elsewhere. Model discovery preserves supported native protocols rather than treating every discovered model as OpenAI chat completions. System One classification delegates to the host Pi implementation.
23
+
24
+ `DASHSCOPE_IMAGE_URL` can override the DashScope image API base URL; its default is `https://dashscope.aliyuncs.com/api/v1`. OpenRouter's image catalog belongs to Pi's built-in provider and is not registered again here. The dedicated `classifier/` catalog is currently reserved; Magpie discovers classifier models dynamically.
25
+
26
+ ## Related packages
27
+
28
+ [image-generate](https://github.com/RolinShmily/pi-kit/tree/main/extensions/image-generate) provides the image-generation tool. [tui-asr](https://github.com/RolinShmily/pi-kit/tree/main/extensions/tui-asr) provides dictation. Their shared helper files do not independently register this provider extension. Store credentials through Pi or environment variables rather than editing source files.
@@ -0,0 +1,154 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { StreamingAsrProvider, VoiceCallbacks } from "../types.ts";
3
+
4
+ export class DashscopeAsrProvider implements StreamingAsrProvider {
5
+ private ws: WebSocket | null = null;
6
+ private taskId: string = "";
7
+ private lastSentenceMap = new Map<number, string>();
8
+ private isCancelled = false;
9
+
10
+ constructor(
11
+ private model: string,
12
+ private apiKey: string,
13
+ ) {}
14
+
15
+ async start(callbacks: VoiceCallbacks): Promise<void> {
16
+ this.isCancelled = false;
17
+ if (!this.apiKey) {
18
+ throw new Error("DashScope API Key not found. Please run /login dashscope or set DASHSCOPE_API_KEY environment variable.");
19
+ }
20
+
21
+ this.taskId = randomUUID().replace(/-/g, "");
22
+ this.lastSentenceMap.clear();
23
+
24
+ const url = "wss://dashscope.aliyuncs.com/api-ws/v1/inference/";
25
+ const ws = new WebSocket(url, {
26
+ headers: {
27
+ Authorization: `Bearer ${this.apiKey}`,
28
+ },
29
+ } as any);
30
+ this.ws = ws;
31
+
32
+ return new Promise((resolve, reject) => {
33
+ let taskStarted = false;
34
+
35
+ ws.addEventListener("open", () => {
36
+ if (this.isCancelled) return;
37
+ const startMsg = {
38
+ header: {
39
+ action: "run-task",
40
+ task_id: this.taskId,
41
+ streaming: "duplex",
42
+ },
43
+ payload: {
44
+ task_group: "audio",
45
+ task: "asr",
46
+ function: "recognition",
47
+ model: this.model,
48
+ parameters: {
49
+ format: "pcm",
50
+ sample_rate: 16000,
51
+ },
52
+ input: {},
53
+ },
54
+ };
55
+ try {
56
+ ws.send(JSON.stringify(startMsg));
57
+ } catch {}
58
+ });
59
+
60
+ ws.addEventListener("message", (ev) => {
61
+ if (this.isCancelled) return;
62
+ try {
63
+ const msg = JSON.parse(ev.data as string);
64
+ const header = msg.header;
65
+ if (header?.event === "task-started") {
66
+ taskStarted = true;
67
+ resolve();
68
+ } else if (header?.event === "result-generated") {
69
+ const sentences = msg.payload?.output?.sentence;
70
+ if (Array.isArray(sentences)) {
71
+ for (let i = 0; i < sentences.length; i++) {
72
+ const s = sentences[i];
73
+ if (s?.text && s.end_time !== undefined) {
74
+ if (!this.lastSentenceMap.has(i)) {
75
+ this.lastSentenceMap.set(i, s.text);
76
+ callbacks.onFinal(s.text.trim());
77
+ }
78
+ }
79
+ }
80
+ }
81
+ } else if (header?.event === "task-finished") {
82
+ const sentences = msg.payload?.output?.sentence;
83
+ if (Array.isArray(sentences)) {
84
+ for (let i = 0; i < sentences.length; i++) {
85
+ const s = sentences[i];
86
+ if (s?.text && !this.lastSentenceMap.has(i)) {
87
+ this.lastSentenceMap.set(i, s.text);
88
+ callbacks.onFinal(s.text.trim());
89
+ }
90
+ }
91
+ }
92
+ callbacks.onClose();
93
+ } else if (header?.event === "task-failed") {
94
+ const err = new Error(header.error_message || "DashScope ASR task failed");
95
+ if (!taskStarted) reject(err);
96
+ else callbacks.onError(err);
97
+ }
98
+ } catch {}
99
+ });
100
+
101
+ ws.addEventListener("error", () => {
102
+ if (this.isCancelled) return;
103
+ const err = new Error("DashScope WebSocket connection error");
104
+ if (!taskStarted) reject(err);
105
+ else callbacks.onError(err);
106
+ });
107
+
108
+ ws.addEventListener("close", () => {
109
+ if (this.isCancelled) return;
110
+ callbacks.onClose();
111
+ });
112
+ });
113
+ }
114
+
115
+ sendAudio(chunk: Buffer): void {
116
+ if (!this.isCancelled && this.ws && this.ws.readyState === WebSocket.OPEN) {
117
+ this.ws.send(chunk);
118
+ }
119
+ }
120
+
121
+ async stop(): Promise<void> {
122
+ if (!this.isCancelled && this.ws && this.ws.readyState === WebSocket.OPEN && this.taskId) {
123
+ try {
124
+ const finishMsg = {
125
+ header: {
126
+ action: "finish-task",
127
+ task_id: this.taskId,
128
+ streaming: "duplex",
129
+ },
130
+ payload: {
131
+ input: {},
132
+ },
133
+ };
134
+ this.ws.send(JSON.stringify(finishMsg));
135
+ } catch {}
136
+ }
137
+ }
138
+
139
+ cancel(): void {
140
+ this.isCancelled = true;
141
+ if (this.ws) {
142
+ try {
143
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
144
+ this.ws.close();
145
+ }
146
+ } catch {}
147
+ this.ws = null;
148
+ }
149
+ }
150
+ }
151
+
152
+ export function createDashscopeAsrProvider(modelId: string, apiKey: string): StreamingAsrProvider {
153
+ return new DashscopeAsrProvider(modelId, apiKey);
154
+ }
@@ -0,0 +1,37 @@
1
+ import { DASHSCOPE_ASR_MODELS } from "./models.ts";
2
+ import { createDashscopeAsrProvider } from "./handlers/dashscope.ts";
3
+ import type { AsrModelDefinition, StreamingAsrProvider } from "./types.ts";
4
+
5
+ export * from "./types.ts";
6
+ export * from "./models.ts";
7
+ export * from "./handlers/dashscope.ts";
8
+
9
+ export interface AsrProviderConfig {
10
+ name: string;
11
+ models: AsrModelDefinition[];
12
+ createProvider: (modelId: string, apiKey: string) => StreamingAsrProvider;
13
+ }
14
+
15
+ export const asrProviders: Record<string, AsrProviderConfig> = {
16
+ dashscope: {
17
+ name: "Aliyun DashScope",
18
+ models: DASHSCOPE_ASR_MODELS,
19
+ createProvider: createDashscopeAsrProvider,
20
+ },
21
+ };
22
+
23
+ export const ALL_ASR_MODELS: AsrModelDefinition[] = [
24
+ ...DASHSCOPE_ASR_MODELS,
25
+ ];
26
+
27
+ export function createAsrProvider(
28
+ providerId: string,
29
+ modelId: string,
30
+ apiKey: string,
31
+ ): StreamingAsrProvider {
32
+ const provider = asrProviders[providerId];
33
+ if (!provider) {
34
+ throw new Error(`Unsupported ASR provider: ${providerId}`);
35
+ }
36
+ return provider.createProvider(modelId, apiKey);
37
+ }
@@ -0,0 +1,18 @@
1
+ import type { AsrModelDefinition } from "./types.ts";
2
+
3
+ export const DASHSCOPE_ASR_MODELS: AsrModelDefinition[] = [
4
+ {
5
+ id: "paraformer-realtime-v2",
6
+ name: "Paraformer Realtime v2",
7
+ provider: "dashscope",
8
+ description: "Real-time speech recognition with high-accuracy punctuation and sentence segmentation (recommended).",
9
+ sampleRate: 16000,
10
+ },
11
+ {
12
+ id: "paraformer-8k-realtime-v1",
13
+ name: "Paraformer 8k Realtime v1",
14
+ provider: "dashscope",
15
+ description: "8k narrowband telephony real-time speech recognition.",
16
+ sampleRate: 8000,
17
+ },
18
+ ];