@huseynsnmz/pi-suite 0.2.2 → 0.2.3
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 +16 -0
- package/README.md +8 -1
- package/package.json +6 -1
- package/packages/board/README.md +1 -1
- package/packages/commands/README.md +1 -1
- package/packages/commands/src/index.ts +22 -0
- package/packages/frame/README.md +8 -2
- package/packages/frame/docs/reference.md +1 -1
- package/packages/frame/src/index.ts +8 -3
- package/packages/frame/src/kit/settings/files.ts +8 -8
- package/packages/frame/src/kit/settings/store.ts +3 -1
- package/packages/frame/src/ui/editor.ts +64 -7
- package/packages/frame/src/ui/history-search.ts +21 -0
- package/packages/frame/src/ui/pi-entries.ts +2 -0
- package/packages/frame/src/ui/thinking.ts +34 -0
- package/packages/memory/README.md +1 -1
- package/packages/modes/README.md +2 -2
- package/packages/modes/src/index.ts +3 -6
- package/packages/router/README.md +29 -9
- package/packages/router/src/config.ts +28 -27
- package/packages/router/src/index.ts +61 -13
- package/packages/router/src/route.ts +16 -10
- package/packages/subscription/LICENSE +21 -0
- package/packages/subscription/README.md +60 -0
- package/packages/subscription/package.json +23 -0
- package/packages/subscription/src/anchors.ts +75 -0
- package/packages/subscription/src/billing.ts +39 -0
- package/packages/subscription/src/catalog.ts +310 -0
- package/packages/subscription/src/cc-sessions.ts +122 -0
- package/packages/subscription/src/config.ts +34 -0
- package/packages/subscription/src/convert.ts +161 -0
- package/packages/subscription/src/index.ts +69 -0
- package/packages/subscription/src/mcp.ts +78 -0
- package/packages/subscription/src/models.ts +65 -0
- package/packages/subscription/src/prompt.ts +57 -0
- package/packages/subscription/src/provider.ts +617 -0
- package/packages/subscription/src/session.ts +294 -0
- package/packages/subscription/src/util.ts +73 -0
- package/packages/web/README.md +5 -5
- package/packages/web/src/config.ts +13 -23
- package/packages/web/src/index.ts +11 -7
- package/packages/web/src/reader.ts +15 -4
- package/packages/web/src/validate.ts +2 -2
- package/src/index.ts +92 -10
- package/src/settings-ui.ts +63 -0
- package/src/settings.ts +116 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.3]
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Router: each role can have its own reasoning level, chosen in `/router` from what that role's model offers.
|
|
8
|
+
- Up and Down with one line typed go through past prompts that start with it. With nothing typed they also reach this folder's other sessions.
|
|
9
|
+
- With `hideThinkingBlock` on, reasoning shows while it's written and collapses when the message is done. `Ctrl+T` opens it.
|
|
10
|
+
- New part, off until you run `/suite on subscription`: Claude models on your Claude login, based on pi-claude-subscription. It asks before anything more is billed as extra usage. See its README for the limits and what Anthropic's terms say.
|
|
11
|
+
|
|
12
|
+
- One settings file, `~/.pi/agent/suite.json`, with a section for each part. `/suite` now opens each part's settings as well as turning it on or off. Old files move in the first time Pi starts, and stay as `.bak`.
|
|
13
|
+
|
|
14
|
+
### Fixes
|
|
15
|
+
|
|
16
|
+
- `router/auto` is always listed in `/model`, and says to run `/router` until `plan` and `build` are set.
|
|
17
|
+
- `/clear` keeps the current model and reasoning level.
|
|
18
|
+
|
|
3
19
|
## [0.2.2]
|
|
4
20
|
|
|
5
21
|
### Fixes
|
package/README.md
CHANGED
|
@@ -11,13 +11,20 @@ One package for [Pi](https://pi.dev): modes and permissions, a framed screen, a
|
|
|
11
11
|
| `frame` | The start card, framed editor, tool boxes, footer and the agent's todo list | [README](packages/frame/README.md) |
|
|
12
12
|
| `board` | A board for starting, watching and attaching to background sessions | [README](packages/board/README.md) |
|
|
13
13
|
| `commands` | `/clear` and `/security-review` | [README](packages/commands/README.md) |
|
|
14
|
+
| `subscription` | Off until you turn it on. Claude models on your Claude login, asking before anything more is billed as extra usage | [README](packages/subscription/README.md) |
|
|
15
|
+
|
|
16
|
+
## Settings
|
|
17
|
+
|
|
18
|
+
Run `/suite` to turn parts on or off and change their settings. They're all kept in `~/.pi/agent/suite.json`, with a section for each part, so you can also edit it by hand. Older files such as `router.json` and `web.json` move in the first time Pi starts, and stay beside it as `.bak`. If one can't be read it stays where it is, and you're told, so a broken file can't silently turn off a setting.
|
|
19
|
+
|
|
20
|
+
There's no setting per project, so a repository can't choose where your code goes. The only exception is the frame's look, in `.pi/extensions/pi-harness/settings.json`, once you trust the project. Pi's own files, and the settings of other packages, such as `pi-permission-system`, stay where they are.
|
|
14
21
|
|
|
15
22
|
## Install
|
|
16
23
|
|
|
17
24
|
Requires Pi 1.5 or newer.
|
|
18
25
|
|
|
19
26
|
```bash
|
|
20
|
-
pi install npm:@huseynsnmz/pi-suite@0.2.
|
|
27
|
+
pi install npm:@huseynsnmz/pi-suite@0.2.3
|
|
21
28
|
```
|
|
22
29
|
|
|
23
30
|
If you installed any of these separately, remove them first:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huseynsnmz/pi-suite",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "One Pi package for all of it: modes and permissions, a framed screen, an agent board, memory, web tools with a separate page reader, and a model router. Turn off what you don't want with /suite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
"README.md",
|
|
29
29
|
"CHANGELOG.md",
|
|
30
30
|
"LICENSE",
|
|
31
|
+
"packages/subscription/src",
|
|
32
|
+
"packages/subscription/package.json",
|
|
33
|
+
"packages/subscription/README.md",
|
|
34
|
+
"packages/subscription/LICENSE",
|
|
31
35
|
"packages/commands/src",
|
|
32
36
|
"packages/commands/package.json",
|
|
33
37
|
"packages/commands/README.md",
|
|
@@ -91,6 +95,7 @@
|
|
|
91
95
|
"test:e2e": "python3 test/e2e/run.py"
|
|
92
96
|
},
|
|
93
97
|
"dependencies": {
|
|
98
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.308",
|
|
94
99
|
"@xterm/headless": "6.0.0",
|
|
95
100
|
"node-pty": "1.1.0",
|
|
96
101
|
"@mozilla/readability": "0.6.0",
|
package/packages/board/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Two commands for Pi. Part of [pi-suite](../../README.md); turn them off with `/s
|
|
|
4
4
|
|
|
5
5
|
## /clear
|
|
6
6
|
|
|
7
|
-
Clears the conversation and starts a new session,
|
|
7
|
+
Clears the conversation and starts a new session, keeping the model and reasoning level you have selected. `/new` goes back to your saved default instead. If the agent is working, it stops first. The old session is kept, and `/resume` brings it back.
|
|
8
8
|
|
|
9
9
|
## /security-review
|
|
10
10
|
|
|
@@ -5,6 +5,14 @@ import { changesToReview, type GitRun, reviewPrompt } from "./security-review.ts
|
|
|
5
5
|
|
|
6
6
|
const REVIEW_MESSAGE = "security-review";
|
|
7
7
|
|
|
8
|
+
/** The model /clear hands to the session it starts. Pi's own new session would take the saved default. */
|
|
9
|
+
const KEPT = Symbol.for("pi-suite:clear-keeps");
|
|
10
|
+
interface Kept {
|
|
11
|
+
provider: string;
|
|
12
|
+
id: string;
|
|
13
|
+
thinking: ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
export default function piCommands(pi: ExtensionAPI): void {
|
|
9
17
|
pi.registerCommand("clear", {
|
|
10
18
|
description: "Clear the conversation and start a new session",
|
|
@@ -13,10 +21,24 @@ export default function piCommands(pi: ExtensionAPI): void {
|
|
|
13
21
|
ctx.abort();
|
|
14
22
|
await ctx.waitForIdle();
|
|
15
23
|
}
|
|
24
|
+
const store = globalThis as Record<symbol, Kept | undefined>;
|
|
25
|
+
if (ctx.model) store[KEPT] = { provider: ctx.model.provider, id: ctx.model.id, thinking: pi.getThinkingLevel() };
|
|
16
26
|
await ctx.newSession();
|
|
27
|
+
delete store[KEPT]; // Still here only if it was cancelled.
|
|
17
28
|
},
|
|
18
29
|
});
|
|
19
30
|
|
|
31
|
+
// Runs in the new session, where this extension is a new instance, so what /clear kept can't live in it.
|
|
32
|
+
pi.on("session_start", async (event, ctx) => {
|
|
33
|
+
const store = globalThis as Record<symbol, Kept | undefined>;
|
|
34
|
+
const kept = store[KEPT];
|
|
35
|
+
delete store[KEPT];
|
|
36
|
+
if (event.reason !== "new" || kept === undefined) return;
|
|
37
|
+
const model = ctx.modelRegistry.find(kept.provider, kept.id);
|
|
38
|
+
if (model !== undefined) await pi.setModel(model);
|
|
39
|
+
pi.setThinkingLevel(kept.thinking);
|
|
40
|
+
});
|
|
41
|
+
|
|
20
42
|
// The diff goes to the model, and only this one line goes on screen.
|
|
21
43
|
pi.registerMessageRenderer<{ summary: string }>(REVIEW_MESSAGE, (message, _options, theme) =>
|
|
22
44
|
new Text(theme.fg("muted", message.details?.summary ?? "Security review"), 1, 0),
|
package/packages/frame/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Requires Pi 1.5 or newer.
|
|
|
19
19
|
2. Install it:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
pi install npm:@huseynsnmz/pi-suite@0.2.
|
|
22
|
+
pi install npm:@huseynsnmz/pi-suite@0.2.3
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
3. Restart Pi. The start card says `π-frame`.
|
|
@@ -44,6 +44,8 @@ Some pieces, such as rooms, boards, peers and the remote page, appear only with
|
|
|
44
44
|
|---|---|
|
|
45
45
|
| `Ctrl+C` | Stops a run and says `Aborted`. With nothing running, it clears the editor and says to press it again within 2 seconds to exit |
|
|
46
46
|
| `Ctrl+R` | Search what you've typed, here and in this folder's other sessions. `Ctrl+R` again goes further back, `Enter` sends, `Esc` cancels, any other key keeps the match to edit. It replaces Pi's Rename on this key; `/name` still renames |
|
|
47
|
+
| `Up` / `Down` | With nothing typed, browse past prompts, including this folder's other sessions. With one line typed, go through prompts that start with it, as in a shell |
|
|
48
|
+
| `Ctrl+T` | Open or collapse finished reasoning |
|
|
47
49
|
| `Alt+O` | Scroll through the session's tool output, one call at a time |
|
|
48
50
|
| `Ctrl+O` | Expand every call in the transcript |
|
|
49
51
|
| `/todos` | Show the agent's todo list. `/todos clear` empties it and tells the agent |
|
|
@@ -78,7 +80,7 @@ More detail is in [How it works](docs/how-it-works.md) and the [reference](docs/
|
|
|
78
80
|
- **`gh` and `glab` still run in their repositories,** and they call `git` for reads such as the remote list.
|
|
79
81
|
- **`glab` needs a host with `gitlab` in its name.** It starts for that remote, and if a token is set in the environment (`GITLAB_TOKEN`), only for `gitlab.com` or `GITLAB_HOST`, because a repository picks its own remote.
|
|
80
82
|
- **The `git status` protections cover the ways to run code that I know of.** New Git options could add others.
|
|
81
|
-
- **The parts of `@adeildo/pi-kit` 6.0.0 that frame uses are copied into `src/kit`,** with its license, so upstream fixes have to be copied in by hand. Its settings stay in `~/.pi/agent/
|
|
83
|
+
- **The parts of `@adeildo/pi-kit` 6.0.0 that frame uses are copied into `src/kit`,** with its license, so upstream fixes have to be copied in by hand. Its settings stay in `~/.pi/agent/suite.json`.
|
|
82
84
|
- **Another session's view isn't fully covered.** Opening one needs other pi-harness packages, and that view hasn't been tested.
|
|
83
85
|
|
|
84
86
|
## Development
|
|
@@ -103,3 +105,7 @@ MIT. See [LICENSE](LICENSE), which keeps the original copyright notice.
|
|
|
103
105
|
- [pi-look and pi-harness](https://github.com/felipeadeildo/pi-harness) by [adeildo](https://adeildo.dev)
|
|
104
106
|
- The framed editor comes from [pi-open-tui](https://github.com/OldSuns/pi-open-tui), by way of pi-look.
|
|
105
107
|
- The segments with priorities come from [oh-my-pi](https://github.com/can1357/oh-my-pi), by way of pi-look.
|
|
108
|
+
|
|
109
|
+
## Reasoning
|
|
110
|
+
|
|
111
|
+
Set `"hideThinkingBlock": true` in `~/.pi/agent/settings.json`, or press `Ctrl+T` until reasoning is collapsed. It then shows while the model is writing it and collapses to one line once the message is done. `Ctrl+T` opens it again, or click it to open just that one. `Ctrl+T` is Pi's own setting, so it applies to every message and stays set in later sessions.
|
|
@@ -27,7 +27,7 @@ Syntax tokens are borrowed on purpose: a theme spends its hues there, so they gi
|
|
|
27
27
|
|
|
28
28
|
## Settings
|
|
29
29
|
|
|
30
|
-
They live
|
|
30
|
+
They live under `frame` in `~/.pi/agent/suite.json`.
|
|
31
31
|
|
|
32
32
|
| Key | Default | Does |
|
|
33
33
|
| ------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from "node:path";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
ACCOUNT,
|
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
ExtensionContext,
|
|
24
24
|
ReadonlyFooterDataProvider,
|
|
25
25
|
} from "@earendil-works/pi-coding-agent";
|
|
26
|
-
import { keyText, parseSkillBlock } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { getAgentDir, keyText, parseSkillBlock } from "@earendil-works/pi-coding-agent";
|
|
27
27
|
import { Key, type KeyId, type TUI } from "@earendil-works/pi-tui";
|
|
28
28
|
|
|
29
29
|
import { type GitState, readGit } from "./data/git.ts";
|
|
@@ -157,7 +157,7 @@ export const frameFeature = defineFeature({
|
|
|
157
157
|
|
|
158
158
|
for (const entry of FRAME_SETTINGS) entry.listen(scope, repaint);
|
|
159
159
|
frame.listen(scope, () => live.ctx && installEditor(live.ctx, screen, live, scope));
|
|
160
|
-
const visitsFile = join(
|
|
160
|
+
const visitsFile = join(getAgentDir(), "extensions", "pi-harness", "frame-visits.json");
|
|
161
161
|
let visited = false;
|
|
162
162
|
let lastVisit: LastVisit | undefined;
|
|
163
163
|
|
|
@@ -527,6 +527,9 @@ function pressCtrlC(live: Live, clearEditor: () => void): void {
|
|
|
527
527
|
live.repaint?.();
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
/** Everything collapsed has finished, because live reasoning stays open. */
|
|
531
|
+
const THOUGHT_LABEL = "Thought · ctrl+t to expand";
|
|
532
|
+
|
|
530
533
|
function installEditor(
|
|
531
534
|
ctx: ExtensionContext,
|
|
532
535
|
screen: Screen,
|
|
@@ -535,9 +538,11 @@ function installEditor(
|
|
|
535
538
|
): void {
|
|
536
539
|
if (ctx.mode !== "tui") return;
|
|
537
540
|
if (frame.get(scope) === "off") {
|
|
541
|
+
ctx.ui.setHiddenThinkingLabel(undefined);
|
|
538
542
|
ctx.ui.setEditorComponent(undefined);
|
|
539
543
|
return;
|
|
540
544
|
}
|
|
545
|
+
ctx.ui.setHiddenThinkingLabel(THOUGHT_LABEL);
|
|
541
546
|
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
542
547
|
live.tui = tui;
|
|
543
548
|
applyCursor(tui, cursor);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Every app shares the settings files, so a write keeps every key it does not know.
|
|
2
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
|
|
5
5
|
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
@@ -14,7 +14,7 @@ export interface SettingsFile {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function globalSettingsPath(): string {
|
|
17
|
-
return join(getAgentDir(), "
|
|
17
|
+
return join(getAgentDir(), "suite.json");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function projectSettingsPath(cwd: string): string {
|
|
@@ -40,12 +40,12 @@ export function writeSettingsFile(
|
|
|
40
40
|
options: { mode?: number } = {},
|
|
41
41
|
): void {
|
|
42
42
|
mkdirSync(dirname(path), { recursive: true });
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
48
|
-
renameSync(temporary,
|
|
43
|
+
// A linked file stays linked, and a private one stays private. Other parts' settings share it.
|
|
44
|
+
const target = existsSync(path) ? realpathSync(path) : path;
|
|
45
|
+
const mode = options.mode ?? (existsSync(target) ? statSync(target).mode & 0o777 : undefined);
|
|
46
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
47
|
+
writeFileSync(temporary, `${JSON.stringify(data, null, "\t")}\n`, { encoding: "utf8", mode });
|
|
48
|
+
renameSync(temporary, target);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function lookup(data: SettingsData, id: string): unknown {
|
|
@@ -135,7 +135,9 @@ export class SettingsStore {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
#edit(path: string, change: (data: SettingsData) => void): string | undefined {
|
|
138
|
-
const { data } = readSettingsFile(path);
|
|
138
|
+
const { data, warnings } = readSettingsFile(path);
|
|
139
|
+
// Reading a broken file gives nothing, and writing that would wipe what else it holds.
|
|
140
|
+
if (warnings.length > 0) return warnings[0];
|
|
139
141
|
change(data);
|
|
140
142
|
try {
|
|
141
143
|
writeSettingsFile(path, data);
|
|
@@ -15,7 +15,15 @@ import { BOXES, border } from "../render/frame.ts";
|
|
|
15
15
|
import { paintLead, promptLead } from "../render/lead.ts";
|
|
16
16
|
import { SETTINGS_KEY, SETTINGS_SHORTCUT_INPUT } from "../render/keys.ts";
|
|
17
17
|
import type { CursorStyle } from "../settings.ts";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
findPrompt,
|
|
20
|
+
type HistorySearch,
|
|
21
|
+
type PrefixSearch,
|
|
22
|
+
shownText,
|
|
23
|
+
startPrefix,
|
|
24
|
+
startSearch,
|
|
25
|
+
stepPrefix,
|
|
26
|
+
} from "./history-search.ts";
|
|
19
27
|
import { type Screen, type SlotName, slotPieces } from "./screen.ts";
|
|
20
28
|
|
|
21
29
|
type WorkingIndicator = Parameters<CustomEditor["setWorkingStatusIndicator"]>[0];
|
|
@@ -44,6 +52,8 @@ export class FrameEditor extends CustomEditor {
|
|
|
44
52
|
#hiddenBelow = 0;
|
|
45
53
|
#inset = 0;
|
|
46
54
|
#search: HistorySearch | undefined;
|
|
55
|
+
#prefix: PrefixSearch | undefined;
|
|
56
|
+
#seeded = false;
|
|
47
57
|
|
|
48
58
|
constructor(
|
|
49
59
|
tui: TUI,
|
|
@@ -77,13 +87,9 @@ export class FrameEditor extends CustomEditor {
|
|
|
77
87
|
this.#searchKey(this.#search, data);
|
|
78
88
|
return;
|
|
79
89
|
}
|
|
90
|
+
if (this.#prefixKey(data)) return;
|
|
80
91
|
if (matchesKey(data, SEARCH_KEY) && !this.isShowingAutocomplete()) {
|
|
81
|
-
|
|
82
|
-
const own: unknown = Reflect.get(this, "history");
|
|
83
|
-
this.#search = startSearch(this.getText(), [
|
|
84
|
-
...(Array.isArray(own) ? (own as string[]) : []),
|
|
85
|
-
...this.#screen.pastPrompts(),
|
|
86
|
-
]);
|
|
92
|
+
this.#search = startSearch(this.getText(), this.#history());
|
|
87
93
|
return;
|
|
88
94
|
}
|
|
89
95
|
if (this.#keybindings.matches(data, "app.clear") && !this.isShowingAutocomplete()) {
|
|
@@ -101,6 +107,51 @@ export class FrameEditor extends CustomEditor {
|
|
|
101
107
|
if (skillQueryAt(this.getLines(), line, col) !== undefined) openAutocomplete(this);
|
|
102
108
|
}
|
|
103
109
|
|
|
110
|
+
/** What Up browses, then what was typed in this folder's other sessions. Read once, on first use. */
|
|
111
|
+
#history(): readonly string[] {
|
|
112
|
+
const own: unknown = Reflect.get(this, "history");
|
|
113
|
+
if (!Array.isArray(own)) return this.#screen.pastPrompts();
|
|
114
|
+
const entries = own as string[];
|
|
115
|
+
if (!this.#seeded) {
|
|
116
|
+
this.#seeded = true;
|
|
117
|
+
for (const prompt of this.#screen.pastPrompts()) {
|
|
118
|
+
if (!entries.includes(prompt)) entries.push(prompt);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return entries;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Up with one line typed looks back through prompts that start with it, and Down comes forward
|
|
126
|
+
* again. Any other key keeps what's showing. An empty prompt is left to pi's own browsing.
|
|
127
|
+
*/
|
|
128
|
+
#prefixKey(data: string): boolean {
|
|
129
|
+
const up = this.#keybindings.matches(data, "tui.editor.cursorUp");
|
|
130
|
+
const down = this.#keybindings.matches(data, "tui.editor.cursorDown");
|
|
131
|
+
if (!up && !down) {
|
|
132
|
+
this.#prefix = undefined;
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (this.isShowingAutocomplete()) return false;
|
|
136
|
+
if (this.#prefix === undefined) {
|
|
137
|
+
const typed = this.getText();
|
|
138
|
+
const entries = this.#history();
|
|
139
|
+
const browsing = Reflect.get(this, "historyIndex") !== -1;
|
|
140
|
+
if (down || typed === "" || typed.includes("\n") || browsing || !onFirstVisualLine(this)) return false;
|
|
141
|
+
const search = startPrefix(typed, entries);
|
|
142
|
+
// With nothing to go through, the arrows stay pi's.
|
|
143
|
+
if (search.matches.length === 0) return false;
|
|
144
|
+
this.#prefix = search;
|
|
145
|
+
}
|
|
146
|
+
const text = stepPrefix(this.#prefix, up ? 1 : -1);
|
|
147
|
+
if (text === undefined && down) {
|
|
148
|
+
this.#prefix = undefined;
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
if (text !== undefined) this.setText(text);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
104
155
|
/** Enter sends the match, Esc goes back, and any other key keeps the match to edit. */
|
|
105
156
|
#searchKey(search: HistorySearch, data: string): void {
|
|
106
157
|
if (matchesKey(data, SEARCH_KEY)) {
|
|
@@ -248,6 +299,12 @@ export class FrameEditor extends CustomEditor {
|
|
|
248
299
|
}
|
|
249
300
|
}
|
|
250
301
|
|
|
302
|
+
/** The method is private. If a pi release renames it, Up just moves the cursor as before. */
|
|
303
|
+
function onFirstVisualLine(editor: CustomEditor): boolean {
|
|
304
|
+
const check: unknown = Reflect.get(editor, "isOnFirstVisualLine");
|
|
305
|
+
return typeof check === "function" && check.call(editor) === true;
|
|
306
|
+
}
|
|
307
|
+
|
|
251
308
|
/** The method is private; nothing opens if a pi release renames it. */
|
|
252
309
|
function openAutocomplete(editor: CustomEditor): void {
|
|
253
310
|
const open: unknown = Reflect.get(editor, "tryTriggerAutocomplete");
|
|
@@ -21,3 +21,24 @@ export function findPrompt(search: HistorySearch, from: number): number {
|
|
|
21
21
|
export function shownText(search: HistorySearch): string {
|
|
22
22
|
return search.at < 0 ? search.draft : (search.entries[search.at] as string);
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
/** Up with text typed: older prompts that start with it, as in a shell. */
|
|
26
|
+
export interface PrefixSearch {
|
|
27
|
+
typed: string;
|
|
28
|
+
matches: readonly string[];
|
|
29
|
+
at: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function startPrefix(typed: string, entries: readonly string[]): PrefixSearch {
|
|
33
|
+
const wanted = typed.toLowerCase();
|
|
34
|
+
const matches = [...new Set(entries)].filter((entry) => entry !== typed && entry.toLowerCase().startsWith(wanted));
|
|
35
|
+
return { typed, matches, at: -1 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One step older (1) or newer (-1), ending back on what was typed. Undefined past either end. */
|
|
39
|
+
export function stepPrefix(search: PrefixSearch, step: 1 | -1): string | undefined {
|
|
40
|
+
const next = search.at + step;
|
|
41
|
+
if (next < -1 || next >= search.matches.length) return undefined;
|
|
42
|
+
search.at = next;
|
|
43
|
+
return next < 0 ? search.typed : search.matches[next];
|
|
44
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Pi's own transcript entries in the harness frame instead of a background. Pi has no hook for
|
|
2
2
|
// drawing them, so this wraps each component's render.
|
|
3
|
+
import { showThinkingWhileLive } from "./thinking.ts";
|
|
3
4
|
import { CHANGES_CUT, CHANGES_X, entryFrame, type EntryFrame, GLYPH } from "../kit/index.ts";
|
|
4
5
|
import {
|
|
5
6
|
BashExecutionComponent,
|
|
@@ -63,6 +64,7 @@ export function framePiEntries(source: EntryPaint): void {
|
|
|
63
64
|
paint = source;
|
|
64
65
|
if (installed) return;
|
|
65
66
|
installed = true;
|
|
67
|
+
showThinkingWhileLive(() => paint() !== undefined);
|
|
66
68
|
wrap(UserMessageComponent, (self, look) =>
|
|
67
69
|
look.you ? youFrame(look, fromOf(self, look), sentInOf(self, look)) : undefined,
|
|
68
70
|
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AssistantMessageComponent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
/** Private in pi. If a release renames them, reasoning just follows pi's setting again. */
|
|
4
|
+
interface Message {
|
|
5
|
+
updateContent(message: { content: readonly { type: string }[] }, isStreaming?: boolean): void;
|
|
6
|
+
isStreaming?: boolean;
|
|
7
|
+
thinkingVisibilityOverrides?: Map<number, boolean>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const live = new WeakSet<object>();
|
|
11
|
+
let isActive: () => boolean = () => false;
|
|
12
|
+
let installed = false;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* With pi's setting that collapses reasoning on, it still shows while it's being written, and
|
|
16
|
+
* collapses once the message is done. Ctrl+T or a click opens it again.
|
|
17
|
+
*/
|
|
18
|
+
export function showThinkingWhileLive(active: () => boolean): void {
|
|
19
|
+
isActive = active;
|
|
20
|
+
if (installed) return;
|
|
21
|
+
installed = true;
|
|
22
|
+
const prototype = AssistantMessageComponent.prototype as unknown as Message;
|
|
23
|
+
const own = prototype.updateContent;
|
|
24
|
+
prototype.updateContent = function updateContent(this: Message, message, isStreaming = this.isStreaming) {
|
|
25
|
+
const overrides = this.thinkingVisibilityOverrides;
|
|
26
|
+
if (overrides !== undefined && isActive() && isStreaming === true) {
|
|
27
|
+
live.add(this);
|
|
28
|
+
message.content.forEach((_part, run) => overrides.set(run, false));
|
|
29
|
+
} else if (overrides !== undefined && live.delete(this)) {
|
|
30
|
+
overrides.clear();
|
|
31
|
+
}
|
|
32
|
+
own.call(this, message, isStreaming);
|
|
33
|
+
};
|
|
34
|
+
}
|
package/packages/modes/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Requires Pi 1.5 or newer.
|
|
|
23
23
|
1. Install it:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
pi install npm:@huseynsnmz/pi-suite@0.2.
|
|
26
|
+
pi install npm:@huseynsnmz/pi-suite@0.2.3
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
2. Move pi-suite above `@gotgenes/pi-permission-system`, because `pi install` adds packages last. Otherwise, in `plan` and `read-only` you may be asked to approve an action that is then blocked anyway. It stays blocked either way, and the extension warns you at startup. Skip this step if you don't use that package.
|
|
@@ -55,7 +55,7 @@ To install from git instead, use `pi install git:github.com/huseynsnmz/pi-suite@
|
|
|
55
55
|
| `/mode default` | Pick the mode new sessions start in |
|
|
56
56
|
| `/mode default plan` | Set it directly |
|
|
57
57
|
|
|
58
|
-
The default is saved as `defaultMode` in `~/.pi/agent/
|
|
58
|
+
The default is saved as `defaultMode` under `modes` in `~/.pi/agent/suite.json`, and you can also set it in `/suite`. A resumed session keeps the mode it was in. `yolo` can't be the default, because it has to be asked for each time.
|
|
59
59
|
|
|
60
60
|
`Shift+Tab` normally cycles the thinking level, and this extension takes it over. To keep that action on another key, add this to `~/.pi/agent/keybindings.json`:
|
|
61
61
|
|
|
@@ -10,6 +10,7 @@ import { registerInterview } from "./interview.ts";
|
|
|
10
10
|
import { registerReviewTools } from "./review/tools.ts";
|
|
11
11
|
import { aliasDefinitions, DENY_OFF_PROMPT, withAliases, withoutAliases } from "./deny-off.ts";
|
|
12
12
|
import { clearGate, hasPermissionSystem, installJudgeGate, JUDGE_NAMES, judgeGated, registerYoloLink, setGateMode, YOLO_LINK } from "./judge-gate.ts";
|
|
13
|
+
import { readSection, writeSection } from "../../../src/settings.ts";
|
|
13
14
|
import { decideToolCall, displayCommand, fromExpectedPackage, notReadOnly, isMode, scriptedChange, loadsAfterPermissionSystem, type Mode, MODES, modePrompt, nextMode, resumedMode, startingMode, SUBMIT_PLAN_TOOL } from "./modes.ts";
|
|
14
15
|
|
|
15
16
|
const STATUS_KEY = "pi-modes";
|
|
@@ -87,20 +88,16 @@ function packagesLoadThisFirst(): boolean {
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
const configPath = () => join(getAgentDir(), "extensions", "pi-modes", "config.json");
|
|
91
|
-
|
|
92
91
|
function readConfig(): Record<string, unknown> {
|
|
93
92
|
try {
|
|
94
|
-
|
|
95
|
-
return typeof config === "object" && config !== null ? (config as Record<string, unknown>) : {};
|
|
93
|
+
return readSection(getAgentDir(), "modes");
|
|
96
94
|
} catch {
|
|
97
95
|
return {};
|
|
98
96
|
}
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
function saveDefaultMode(mode: Mode): void {
|
|
102
|
-
|
|
103
|
-
writeFileSync(configPath(), `${JSON.stringify({ ...readConfig(), defaultMode: mode }, null, 2)}\n`);
|
|
100
|
+
writeSection(getAgentDir(), "modes", { ...readConfig(), defaultMode: mode });
|
|
104
101
|
}
|
|
105
102
|
|
|
106
103
|
function restoredMode(ctx: ExtensionContext): Mode {
|
|
@@ -9,10 +9,12 @@ Adds `router/auto` to [Pi](https://pi.dev): one choice in `/model` that uses a s
|
|
|
9
9
|
Requires Pi 1.5 or newer.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
pi install npm:@huseynsnmz/pi-suite@0.2.
|
|
12
|
+
pi install npm:@huseynsnmz/pi-suite@0.2.3
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Log in to the providers you want with `/login`, run `/router` to choose a model for each role, then pick `router/auto` in `/model`.
|
|
15
|
+
Log in to the providers you want with `/login`, run `/router` to choose a model for each role, then pick `router/auto` in `/model`. It's always listed there, and until `plan` and `build` are set it stops with a message telling you to run `/router`.
|
|
16
|
+
|
|
17
|
+
In `/model`, `Enter` selects a model for this session and `Ctrl+S` saves it as your default for new sessions.
|
|
16
18
|
|
|
17
19
|
## Roles
|
|
18
20
|
|
|
@@ -25,17 +27,35 @@ Log in to the providers you want with `/login`, run `/router` to choose a model
|
|
|
25
27
|
| `subagent` | Runs subagents that use `router/auto` | Same as `build` |
|
|
26
28
|
| `fallback` | Takes over when a provider is overloaded or rate limited | Retries on the same model |
|
|
27
29
|
|
|
28
|
-
`/router` saves them in `~/.pi/agent/
|
|
30
|
+
`/router` saves them under `router` in `~/.pi/agent/suite.json`, apart from `judge`, which goes in the judge's own config. `/suite` opens the same screen.
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"router": {
|
|
35
|
+
"plan": "anthropic/claude-opus-5-5",
|
|
36
|
+
"build": "anthropic/claude-haiku-5-5",
|
|
37
|
+
"fallback": "openai/gpt-5.6-luna"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
There's no setting per project, so a repository can't choose where your code gets sent.
|
|
43
|
+
|
|
44
|
+
## Reasoning per role
|
|
45
|
+
|
|
46
|
+
After you choose a model for a role, `/router` asks how much reasoning it should use, offering only the levels that model has. It's saved next to the roles:
|
|
29
47
|
|
|
30
48
|
```json
|
|
31
49
|
{
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
"router": {
|
|
51
|
+
"plan": "anthropic/claude-opus-5-5",
|
|
52
|
+
"build": "anthropic/claude-haiku-5-5",
|
|
53
|
+
"thinking": { "plan": "high", "build": "low" }
|
|
54
|
+
}
|
|
35
55
|
}
|
|
36
56
|
```
|
|
37
57
|
|
|
38
|
-
|
|
58
|
+
A role with no level of its own uses the one you pick with `/effort`. Changing a role's model clears its level, because the new model may not have it. A retry keeps the level of the request that failed. `reader` and `subagent` with no model of their own use `build`'s level. `judge` has no level here, as it's set in the judge's own config.
|
|
39
59
|
|
|
40
60
|
## How it decides
|
|
41
61
|
|
|
@@ -50,7 +70,7 @@ There's no setting per project, so a repository can't choose where your code get
|
|
|
50
70
|
- Moving from `plan` to `build` happens once in a session. It loses the prompt cache one time, as any change of model does.
|
|
51
71
|
- The session stays on `build` after that. For a hard follow-up, switch to `plan` mode, or start a new session.
|
|
52
72
|
- If a request fails because the provider is overloaded, it moves to `fallback` for the rest of that turn. The next message tries the first choice again. Any other failure retries on the same model.
|
|
53
|
-
- A role whose model is missing or has no login stops that request with an error naming it, and so
|
|
73
|
+
- A role whose model is missing or has no login stops that request with an error naming it, and so do broken router settings. The router doesn't pick another model for you.
|
|
54
74
|
- Leaving `plan` mode counts as finishing planning, whether you approved a plan or changed mode.
|
|
55
75
|
- The roles only apply while `router/auto` is selected. On any other model, nothing goes to them, and pages are read as if the router weren't there.
|
|
56
76
|
- `/suite off router` moves the session to the model that last answered. If `router/auto` is your saved default, later sessions use another available model, as Pi does for any model that no longer exists.
|
|
@@ -61,7 +81,7 @@ The phase follows the session tree, so a branch keeps its own.
|
|
|
61
81
|
|
|
62
82
|
**`modes`:** tells the router the mode. With it off, the router only goes by the first edit.
|
|
63
83
|
|
|
64
|
-
**`web`:** your `readerModel` in
|
|
84
|
+
**`web`:** your `readerModel` in the web settings still wins over the `reader` role.
|
|
65
85
|
|
|
66
86
|
**`frame`:** shows who answered, such as `Auto → Haiku`. Pi's own footer already does.
|
|
67
87
|
|