pi-bro 0.26.0 → 0.28.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.
- package/CHANGELOG.md +31 -0
- package/README.md +6 -1
- package/docs/README.md +2 -0
- package/docs/grill-me.md +63 -0
- package/docs/reference/commands.md +10 -3
- package/docs/reference/data-and-permissions.md +1 -0
- package/docs/reference/settings.md +4 -0
- package/docs/remember-me.md +90 -0
- package/docs/user-guide.md +4 -0
- package/package.json +12 -2
- package/src/bro.ts +59 -40
- package/src/btw.ts +27 -5
- package/src/explain-modal.ts +7 -2
- package/src/grill-engine.ts +116 -0
- package/src/grill.ts +312 -0
- package/src/remember-engine.ts +141 -0
- package/src/remember-store.ts +193 -0
- package/src/remember-tool.ts +22 -0
- package/src/remember.ts +294 -0
- package/src/simplify-engine.ts +37 -0
- package/src/simplify-ui.ts +156 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/** Source-focused Simplify modal. No chat thread, durable draft or new backend boundary. */
|
|
2
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { stripVTControlCharacters } from "node:util";
|
|
4
|
+
import { copyToClipboard } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
6
|
+
import { BtwModal } from "./btw.ts";
|
|
7
|
+
import { latestAssistant, simplify, type BroResult } from "./explain-modal.ts";
|
|
8
|
+
import { openBroOverlay } from "./modal-kit.ts";
|
|
9
|
+
import { nextBroMode, type BroMode } from "./prompt.ts";
|
|
10
|
+
import { readSettings } from "./settings.ts";
|
|
11
|
+
import { extractDocumentText, extractWebPage, type BroSource } from "./sources.ts";
|
|
12
|
+
import { hasBroCustomUi } from "./ui-capabilities.ts";
|
|
13
|
+
import { errorMessage, unquote } from "./util.ts";
|
|
14
|
+
import { parseSimplifyAction, SIMPLIFY_MODAL_HELP, type SimplifyAction } from "./simplify-engine.ts";
|
|
15
|
+
|
|
16
|
+
export type SimplifyState = {
|
|
17
|
+
result?: BroResult;
|
|
18
|
+
view?: "result" | "help" | "empty";
|
|
19
|
+
draft?: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
};
|
|
22
|
+
export type SimplifyStart =
|
|
23
|
+
| { kind:"resume" }
|
|
24
|
+
| { kind:"latest" }
|
|
25
|
+
| { kind:"text"|"file"|"url"; input:string };
|
|
26
|
+
|
|
27
|
+
async function resolveSource(ctx: ExtensionCommandContext, request: SimplifyStart | SimplifyAction,
|
|
28
|
+
signal: AbortSignal, retained?: BroSource): Promise<BroSource> {
|
|
29
|
+
if (retained) return retained;
|
|
30
|
+
switch (request.kind) {
|
|
31
|
+
case "latest": {
|
|
32
|
+
await ctx.waitForIdle();
|
|
33
|
+
if (signal.aborted) throw new Error("Cancelled.");
|
|
34
|
+
const source=latestAssistant(ctx);
|
|
35
|
+
if (!source) throw new Error("No completed assistant reply to simplify.");
|
|
36
|
+
return {text:source.text,label:"Latest assistant reply"};
|
|
37
|
+
}
|
|
38
|
+
case "text": return {text:request.input,label:"Pasted text"};
|
|
39
|
+
case "file": return {text:await extractDocumentText(request.input,ctx.cwd,signal),label:unquote(request.input)};
|
|
40
|
+
case "url": return await extractWebPage(request.input,signal);
|
|
41
|
+
default: throw new Error("Choose a source with /simplify, /text, /file or /url.");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The state belongs to the parent /bro extension instance and resets on session_start. */
|
|
46
|
+
export async function openSimplifyWorkflow(
|
|
47
|
+
ctx: ExtensionCommandContext,
|
|
48
|
+
state: SimplifyState,
|
|
49
|
+
initial: SimplifyStart,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
if (!hasBroCustomUi(ctx)) {
|
|
52
|
+
if (initial.kind==="resume") { ctx.ui.notify("Simplify's interactive view requires Pi TUI.", "warning"); return; }
|
|
53
|
+
const controller=new AbortController();
|
|
54
|
+
const source=await resolveSource(ctx,initial,controller.signal);
|
|
55
|
+
const result=await simplify(source.text,controller.signal,await readSettings());
|
|
56
|
+
state.result={source,...result};
|
|
57
|
+
state.view="result"; state.error=undefined;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await openBroOverlay<void>(ctx,(tui,theme,_keys,done)=>{
|
|
62
|
+
let closed=false;
|
|
63
|
+
let controller:AbortController|undefined;
|
|
64
|
+
let running=false;
|
|
65
|
+
const close=()=>{if(closed)return; closed=true;state.draft=modal.getComposerText();controller?.abort();done(undefined);};
|
|
66
|
+
const modal=new BtwModal(tui,theme,close,submit,()=>{
|
|
67
|
+
closed=true;state.draft=modal.getComposerText();controller?.abort();
|
|
68
|
+
},{
|
|
69
|
+
title:"Bro · simplify",
|
|
70
|
+
commands:"",
|
|
71
|
+
inputLabel:"Simplify command",
|
|
72
|
+
idleHint:"Enter run · M change mode · R retry · /copy · /help · Esc close",
|
|
73
|
+
});
|
|
74
|
+
const display=()=>{
|
|
75
|
+
modal.setCustomMode(state.result?.mode ? state.result.mode+(state.result.preferences ? " · prefs" : "") : "one source · one explanation");
|
|
76
|
+
modal.setModel(state.result?.model ?? "");
|
|
77
|
+
if(state.view==="help")modal.setText(SIMPLIFY_MODAL_HELP);
|
|
78
|
+
else if(state.view==="result"&&state.result)modal.setText((state.result.source.label ? "**Source:** "+stripVTControlCharacters(state.result.source.label).replace(/[\r\n]/g," ")+"\n\n---\n\n" : "")+state.result.text);
|
|
79
|
+
else modal.setText("## Simplify\n\nUse /simplify for the latest reply, /text, /file or /url for another source. Type /help for all commands.");
|
|
80
|
+
// Notices describe the currently shown state, not an earlier request.
|
|
81
|
+
modal.setNotice(state.error ?? "",state.error ? "error" : "info");
|
|
82
|
+
modal.scrollToEnd();
|
|
83
|
+
};
|
|
84
|
+
const run=async (request:SimplifyStart|SimplifyAction, retained?:BroSource, mode?:BroMode, submitted?:string)=>{
|
|
85
|
+
if(running||closed)return;
|
|
86
|
+
const attempt=new AbortController();
|
|
87
|
+
controller=attempt; running=true;
|
|
88
|
+
modal.setRunning(true);modal.setNotice("Simplifying…");
|
|
89
|
+
const previous=state.result,previousView=state.view;
|
|
90
|
+
try {
|
|
91
|
+
const source=await resolveSource(ctx,request,attempt.signal,retained);
|
|
92
|
+
if(closed||attempt.signal.aborted)return;
|
|
93
|
+
const currentSettings=await readSettings();
|
|
94
|
+
const result=await simplify(source.text,attempt.signal,currentSettings,text=>{
|
|
95
|
+
if(!closed&&!attempt.signal.aborted)modal.setText(text);
|
|
96
|
+
},mode);
|
|
97
|
+
if(closed||attempt.signal.aborted)return;
|
|
98
|
+
state.result={source,...result};
|
|
99
|
+
state.view="result";state.error=undefined;
|
|
100
|
+
display();
|
|
101
|
+
if(mode && mode !== currentSettings.mode)modal.setNotice("Mode: "+mode+" (not saved)");
|
|
102
|
+
} catch(error){
|
|
103
|
+
if(closed||attempt.signal.aborted)return;
|
|
104
|
+
state.result=previous;state.view=previousView;
|
|
105
|
+
state.error=errorMessage(error);
|
|
106
|
+
display();
|
|
107
|
+
// Failed replacement is editable and retryable. Do not overwrite text the user
|
|
108
|
+
// entered while this request was running. Preserve it on modal close as well.
|
|
109
|
+
if(submitted && !modal.getComposerText().trim()) {
|
|
110
|
+
modal.setComposerText(submitted);
|
|
111
|
+
state.draft=submitted;
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
if(controller===attempt)controller=undefined;
|
|
115
|
+
running=false;
|
|
116
|
+
if(!closed)modal.setRunning(false);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const retry=(switchMode=false)=>{
|
|
120
|
+
if(running||!state.result)return;
|
|
121
|
+
const next=switchMode?nextBroMode(state.result.mode??"balanced"):state.result.mode;
|
|
122
|
+
void run({kind:"latest"},state.result.source,next);
|
|
123
|
+
};
|
|
124
|
+
const copy=()=>{
|
|
125
|
+
if(!state.result?.text){modal.setNotice("Nothing to copy yet.","warning");return;}
|
|
126
|
+
void copyToClipboard(state.result.text)
|
|
127
|
+
.then(()=>{if(!closed)modal.setNotice("Explanation copied.","success");})
|
|
128
|
+
.catch(error=>{if(!closed)modal.setNotice("Copy failed: "+errorMessage(error),"error");});
|
|
129
|
+
};
|
|
130
|
+
function submit(raw:string):void {
|
|
131
|
+
if(running||closed)return;
|
|
132
|
+
const action=parseSimplifyAction(raw);
|
|
133
|
+
if(action.kind==="invalid"){modal.setNotice(action.message,"warning");return;}
|
|
134
|
+
modal.clearComposer();state.draft=undefined;
|
|
135
|
+
if(action.kind==="copy"){copy();return;}
|
|
136
|
+
if(action.kind==="clear"){
|
|
137
|
+
state.result=undefined;state.view="empty";state.error=undefined;display();return;
|
|
138
|
+
}
|
|
139
|
+
if(action.kind==="help"){state.view="help";state.error=undefined;display();return;}
|
|
140
|
+
if(action.kind==="retry"){retry();return;}
|
|
141
|
+
void run(action,undefined,undefined,raw);
|
|
142
|
+
}
|
|
143
|
+
modal.onKey=(data:string)=>{
|
|
144
|
+
if(running||modal.getComposerText().trim())return false;
|
|
145
|
+
if(matchesKey(data,"m")||matchesKey(data,"shift+m")){retry(true);return Boolean(state.result);}
|
|
146
|
+
if(matchesKey(data,"r")||matchesKey(data,"shift+r")){retry();return Boolean(state.result);}
|
|
147
|
+
if(matchesKey(data,"c")||matchesKey(data,"shift+c")){copy();return true;}
|
|
148
|
+
return false;
|
|
149
|
+
};
|
|
150
|
+
display();
|
|
151
|
+
if(state.draft)modal.setComposerText(state.draft);
|
|
152
|
+
if(initial.kind!=="resume")queueMicrotask(()=>{if(!closed)void run(initial,undefined,undefined,
|
|
153
|
+
initial.kind==="latest" ? "/simplify" : "/"+initial.kind+" "+initial.input);});
|
|
154
|
+
return modal;
|
|
155
|
+
},{mouse:true});
|
|
156
|
+
}
|