@mrclrchtr/supi-extras 1.3.1 → 1.4.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/README.md +60 -25
- package/node_modules/@mrclrchtr/supi-core/README.md +52 -41
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +13 -13
- package/node_modules/@mrclrchtr/supi-core/src/{config-settings.ts → config/config-settings.ts} +2 -2
- package/node_modules/@mrclrchtr/supi-core/src/{context-provider-registry.ts → context/context-provider-registry.ts} +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +13 -13
- package/node_modules/@mrclrchtr/supi-core/src/{settings-registry.ts → settings/settings-registry.ts} +1 -1
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/model-effort-colors-helpers.ts +277 -0
- package/src/model-effort-colors.ts +178 -0
- package/src/tab-spinner.ts +57 -20
- /package/node_modules/@mrclrchtr/supi-core/src/{config.ts → config/config.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{context-tag.ts → context/context-tag.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{settings-ui.ts → settings/settings-ui.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,44 +1,79 @@
|
|
|
1
1
|
# @mrclrchtr/supi-extras
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Adds a bundle of small quality-of-life features to the [pi coding agent](https://github.com/earendil-works/pi).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:@mrclrchtr/supi-extras
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
For local development:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pi install ./packages/supi-extras
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
After editing the source, run `/reload`.
|
|
6
18
|
|
|
7
19
|
## What you get
|
|
8
20
|
|
|
9
|
-
|
|
21
|
+
This package mixes a few commands and shortcuts with a few always-on UI tweaks.
|
|
10
22
|
|
|
11
|
-
|
|
23
|
+
## Commands
|
|
12
24
|
|
|
13
|
-
|
|
25
|
+
- `/exit` — exit pi
|
|
26
|
+
- `/e` — alias for `/exit`
|
|
27
|
+
- `/clear` — start a new session (alias for `/new`)
|
|
28
|
+
- `/supi-stash` — browse, restore, copy, delete, or clear saved prompt drafts
|
|
14
29
|
|
|
15
|
-
|
|
30
|
+
## Shortcuts
|
|
16
31
|
|
|
17
|
-
|
|
32
|
+
- `Alt+S` — stash the current editor text
|
|
33
|
+
- `Alt+C` — copy the current editor text to the system clipboard
|
|
34
|
+
- `$skill-name` — input shorthand that expands to `/skill:skill-name`
|
|
18
35
|
|
|
19
|
-
|
|
36
|
+
The `$skill-name` helper also adds skill-only autocomplete while the cursor is inside a `$...` token.
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
## Prompt stash
|
|
22
39
|
|
|
23
|
-
|
|
40
|
+
Prompt stash stores drafts in `~/.pi/agent/supi/prompt-stash.json` so they survive restarts.
|
|
24
41
|
|
|
25
|
-
|
|
42
|
+
`/supi-stash` opens an overlay with these actions:
|
|
26
43
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
44
|
+
- `Enter` — restore the selected draft into the editor
|
|
45
|
+
- `c` — copy the selected draft to the clipboard
|
|
46
|
+
- `d` — delete the selected draft
|
|
47
|
+
- `D` — clear all drafts
|
|
48
|
+
- `Esc` — close the overlay
|
|
49
|
+
|
|
50
|
+
If the stash file cannot be read or written, the feature degrades to in-memory use instead of breaking the extension.
|
|
51
|
+
|
|
52
|
+
## Passive behavior
|
|
53
|
+
|
|
54
|
+
### Tab-title spinner
|
|
55
|
+
|
|
56
|
+
While the agent is working, the package animates a spinner in the terminal tab title. When the turn finishes, it shows a done marker. If `ask_user` is active, the spinner pauses so the waiting-for-input title is not overwritten.
|
|
57
|
+
|
|
58
|
+
### Footer model and effort colors
|
|
59
|
+
|
|
60
|
+
The footer keeps pi's existing information but recolors the active model and reasoning level using theme tokens.
|
|
61
|
+
|
|
62
|
+
### Headless git safety
|
|
63
|
+
|
|
64
|
+
The package sets:
|
|
30
65
|
|
|
31
|
-
|
|
66
|
+
- `GIT_EDITOR=true`
|
|
67
|
+
- `GIT_SEQUENCE_EDITOR=true`
|
|
32
68
|
|
|
33
|
-
|
|
69
|
+
That prevents git subprocesses from hanging while waiting for an interactive editor.
|
|
34
70
|
|
|
35
|
-
|
|
36
|
-
|-----|--------|
|
|
37
|
-
| `↑↓` | Navigate stashed drafts |
|
|
38
|
-
| `Enter` | Restore selected draft to editor |
|
|
39
|
-
| `c` | Copy to clipboard |
|
|
40
|
-
| `d` | Delete (list refreshes in-place) |
|
|
41
|
-
| `D` | Clear all stashes |
|
|
42
|
-
| `Esc` | Cancel |
|
|
71
|
+
## Source
|
|
43
72
|
|
|
44
|
-
|
|
73
|
+
- `src/aliases.ts` — command aliases
|
|
74
|
+
- `src/prompt-stash.ts` — prompt stash shortcuts, persistence, and overlay
|
|
75
|
+
- `src/skill-shortcut.ts` — `$skill-name` expansion and autocomplete
|
|
76
|
+
- `src/tab-spinner.ts` — terminal tab-title spinner
|
|
77
|
+
- `src/copy-prompt.ts` and `src/clipboard.ts` — copy-to-clipboard shortcut and helper
|
|
78
|
+
- `src/model-effort-colors.ts` — footer recoloring
|
|
79
|
+
- `src/git-editor.ts` — git editor environment guard
|
|
@@ -1,65 +1,78 @@
|
|
|
1
1
|
# @mrclrchtr/supi-core
|
|
2
2
|
|
|
3
|
-
Shared infrastructure for SuPi
|
|
3
|
+
Shared infrastructure for SuPi extensions.
|
|
4
|
+
|
|
5
|
+
This package is mainly for extension authors. It gives you a common config system, settings plumbing, context helpers, registries, and a small extension surface that registers `/supi-settings`.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
### As a dependency for another extension
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
12
|
pnpm add @mrclrchtr/supi-core
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`@mrclrchtr/supi-core` now has two explicit surfaces:
|
|
15
|
+
### As a pi package
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
`pi.extensions` still points at the real file path `./src/extension.ts` inside the package. The `/api` and `/extension` paths are consumer-facing package exports, not manifest aliases.
|
|
17
|
+
```bash
|
|
18
|
+
pi install npm:@mrclrchtr/supi-core
|
|
19
|
+
```
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
Installing it as a pi package adds the minimal `/supi-settings` extension surface.
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
## Package surfaces
|
|
25
24
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
- the shared settings registry, overlay UI, and `registerSettingsCommand()` helper
|
|
29
|
-
- XML `<extension-context>` wrapping plus context-message utilities
|
|
30
|
-
- context-provider and debug-event registries reused across SuPi packages
|
|
31
|
-
- project root and path helpers reused by packages such as `supi-lsp`
|
|
25
|
+
- `@mrclrchtr/supi-core/api` — reusable helpers for other packages and extensions
|
|
26
|
+
- `@mrclrchtr/supi-core/extension` — minimal pi extension that registers `/supi-settings`
|
|
32
27
|
|
|
33
|
-
##
|
|
28
|
+
## What you get from the API
|
|
34
29
|
|
|
35
|
-
Config
|
|
30
|
+
### Config helpers
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
- `loadSupiConfig()` — merged config with resolution order `defaults <- global <- project`
|
|
33
|
+
- `loadSupiConfigForScope()` — load one scope at a time for settings UIs
|
|
34
|
+
- `writeSupiConfig()` — persist values
|
|
35
|
+
- `removeSupiConfigKey()` — remove a key or override
|
|
40
36
|
|
|
41
37
|
Config file locations:
|
|
42
38
|
|
|
43
39
|
- global: `~/.pi/agent/supi/config.json`
|
|
44
40
|
- project: `.pi/supi/config.json`
|
|
45
41
|
|
|
46
|
-
|
|
42
|
+
### Settings helpers
|
|
43
|
+
|
|
44
|
+
- `registerSettings()` — register an arbitrary settings section
|
|
45
|
+
- `registerConfigSettings()` — register a config-backed settings section with scoped persistence helpers
|
|
46
|
+
- `registerSettingsCommand()` — register `/supi-settings`
|
|
47
|
+
- `openSettingsOverlay()` — open the shared settings UI directly
|
|
48
|
+
- `createInputSubmenu()` — helper for simple text-entry submenus
|
|
49
|
+
|
|
50
|
+
The built-in settings UI supports:
|
|
47
51
|
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
- `removeSupiConfigKey()`
|
|
52
|
-
- `registerConfigSettings()`
|
|
52
|
+
- project/global scope toggle
|
|
53
|
+
- grouped extension sections
|
|
54
|
+
- searchable setting lists
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
### Context helpers
|
|
55
57
|
|
|
56
|
-
- `wrapExtensionContext()`
|
|
58
|
+
- `wrapExtensionContext()` — wrap injected text in SuPi's `<extension-context>` tag
|
|
57
59
|
- `findLastUserMessageIndex()`
|
|
58
60
|
- `getContextToken()`
|
|
61
|
+
- `getPromptContent()`
|
|
59
62
|
- `pruneAndReorderContextMessages()`
|
|
60
|
-
- `
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
- `restorePromptContent()`
|
|
64
|
+
|
|
65
|
+
### Shared registries
|
|
66
|
+
|
|
67
|
+
- context-provider registry for `/supi-context`
|
|
68
|
+
- debug-event registry for producers that want shared debug capture
|
|
69
|
+
- settings registry used by `/supi-settings`
|
|
70
|
+
|
|
71
|
+
### Project and session helpers
|
|
72
|
+
|
|
73
|
+
- project-root detection and directory walking helpers such as `findProjectRoot()` and `walkProject()`
|
|
74
|
+
- active-branch session helper: `getActiveBranchEntries()`
|
|
75
|
+
- terminal helpers such as `formatTitle()`, `signalWaiting()`, and `signalDone()`
|
|
63
76
|
|
|
64
77
|
## Example
|
|
65
78
|
|
|
@@ -80,17 +93,15 @@ registerConfigSettings({
|
|
|
80
93
|
});
|
|
81
94
|
|
|
82
95
|
const message = wrapExtensionContext("my-extension", "hello", {
|
|
83
|
-
turn: 1,
|
|
84
96
|
file: "CLAUDE.md",
|
|
97
|
+
turn: 1,
|
|
85
98
|
});
|
|
86
99
|
```
|
|
87
100
|
|
|
88
|
-
## Requirements
|
|
89
|
-
|
|
90
|
-
- `@earendil-works/pi-coding-agent`
|
|
91
|
-
- `@earendil-works/pi-tui`
|
|
92
|
-
|
|
93
101
|
## Source
|
|
94
102
|
|
|
95
|
-
-
|
|
96
|
-
-
|
|
103
|
+
- `src/api.ts` — exported library surface
|
|
104
|
+
- `src/extension.ts` — minimal `/supi-settings` entrypoint
|
|
105
|
+
- `src/config.ts` — shared config loading and writing
|
|
106
|
+
- `src/config-settings.ts` — config-backed settings registration helper
|
|
107
|
+
- `src/settings-ui.ts` — shared settings overlay
|
|
@@ -2,30 +2,30 @@
|
|
|
2
2
|
// Provides XML context tag wrapping, unified config system, context-message utilities,
|
|
3
3
|
// and settings registry for supi-wide TUI settings.
|
|
4
4
|
|
|
5
|
-
export type { SupiConfigLocation, SupiConfigOptions } from "./config.ts";
|
|
5
|
+
export type { SupiConfigLocation, SupiConfigOptions } from "./config/config.ts";
|
|
6
6
|
export {
|
|
7
7
|
loadSupiConfig,
|
|
8
8
|
loadSupiConfigForScope,
|
|
9
9
|
removeSupiConfigKey,
|
|
10
10
|
writeSupiConfig,
|
|
11
|
-
} from "./config.ts";
|
|
12
|
-
export type { ConfigSettingsHelpers, ConfigSettingsOptions } from "./config-settings.ts";
|
|
13
|
-
export { registerConfigSettings } from "./config-settings.ts";
|
|
14
|
-
export type { ContextMessageLike } from "./context-messages.ts";
|
|
11
|
+
} from "./config/config.ts";
|
|
12
|
+
export type { ConfigSettingsHelpers, ConfigSettingsOptions } from "./config/config-settings.ts";
|
|
13
|
+
export { registerConfigSettings } from "./config/config-settings.ts";
|
|
14
|
+
export type { ContextMessageLike } from "./context/context-messages.ts";
|
|
15
15
|
export {
|
|
16
16
|
findLastUserMessageIndex,
|
|
17
17
|
getContextToken,
|
|
18
18
|
getPromptContent,
|
|
19
19
|
pruneAndReorderContextMessages,
|
|
20
20
|
restorePromptContent,
|
|
21
|
-
} from "./context-messages.ts";
|
|
22
|
-
export type { ContextProvider } from "./context-provider-registry.ts";
|
|
21
|
+
} from "./context/context-messages.ts";
|
|
22
|
+
export type { ContextProvider } from "./context/context-provider-registry.ts";
|
|
23
23
|
export {
|
|
24
24
|
clearRegisteredContextProviders,
|
|
25
25
|
getRegisteredContextProviders,
|
|
26
26
|
registerContextProvider,
|
|
27
|
-
} from "./context-provider-registry.ts";
|
|
28
|
-
export { wrapExtensionContext } from "./context-tag.ts";
|
|
27
|
+
} from "./context/context-provider-registry.ts";
|
|
28
|
+
export { wrapExtensionContext } from "./context/context-tag.ts";
|
|
29
29
|
export type {
|
|
30
30
|
DebugAgentAccess,
|
|
31
31
|
DebugEvent,
|
|
@@ -64,14 +64,14 @@ export {
|
|
|
64
64
|
walkProject,
|
|
65
65
|
} from "./project-roots.ts";
|
|
66
66
|
export { getActiveBranchEntries } from "./session-utils.ts";
|
|
67
|
-
export { registerSettingsCommand } from "./settings-command.ts";
|
|
68
|
-
export type { SettingsScope, SettingsSection } from "./settings-registry.ts";
|
|
67
|
+
export { registerSettingsCommand } from "./settings/settings-command.ts";
|
|
68
|
+
export type { SettingsScope, SettingsSection } from "./settings/settings-registry.ts";
|
|
69
69
|
export {
|
|
70
70
|
clearRegisteredSettings,
|
|
71
71
|
getRegisteredSettings,
|
|
72
72
|
registerSettings,
|
|
73
|
-
} from "./settings-registry.ts";
|
|
74
|
-
export { createInputSubmenu, openSettingsOverlay } from "./settings-ui.ts";
|
|
73
|
+
} from "./settings/settings-registry.ts";
|
|
74
|
+
export { createInputSubmenu, openSettingsOverlay } from "./settings/settings-ui.ts";
|
|
75
75
|
export type { TitleTarget } from "./terminal.ts";
|
|
76
76
|
export {
|
|
77
77
|
DONE_SYMBOL,
|
package/node_modules/@mrclrchtr/supi-core/src/{config-settings.ts → config/config-settings.ts}
RENAMED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Wraps registerSettings() and centralizes selected-scope loading + scoped persistence.
|
|
3
3
|
|
|
4
4
|
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
5
|
+
import type { SettingsScope } from "../settings/settings-registry.ts";
|
|
6
|
+
import { registerSettings } from "../settings/settings-registry.ts";
|
|
5
7
|
import { loadSupiConfigForScope, removeSupiConfigKey, writeSupiConfig } from "./config.ts";
|
|
6
|
-
import type { SettingsScope } from "./settings-registry.ts";
|
|
7
|
-
import { registerSettings } from "./settings-registry.ts";
|
|
8
8
|
|
|
9
9
|
export interface ConfigSettingsHelpers {
|
|
10
10
|
/** Write a key to the selected scope's config section. */
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Extensions declare context data providers via `registerContextProvider()` during their
|
|
4
4
|
// factory function. The `/supi-context` command reads them via `getRegisteredContextProviders()`.
|
|
5
5
|
|
|
6
|
-
import { createRegistry } from "
|
|
6
|
+
import { createRegistry } from "../registry-utils.ts";
|
|
7
7
|
|
|
8
8
|
export interface ContextProvider {
|
|
9
9
|
/** Unique identifier — e.g. "rtk" */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { registerSettingsCommand as default } from "./settings-command.ts";
|
|
1
|
+
export { registerSettingsCommand as default } from "./settings/settings-command.ts";
|
|
@@ -2,30 +2,30 @@
|
|
|
2
2
|
// Provides XML context tag wrapping, unified config system, context-message utilities,
|
|
3
3
|
// and settings registry for supi-wide TUI settings.
|
|
4
4
|
|
|
5
|
-
export type { SupiConfigLocation, SupiConfigOptions } from "./config.ts";
|
|
5
|
+
export type { SupiConfigLocation, SupiConfigOptions } from "./config/config.ts";
|
|
6
6
|
export {
|
|
7
7
|
loadSupiConfig,
|
|
8
8
|
loadSupiConfigForScope,
|
|
9
9
|
removeSupiConfigKey,
|
|
10
10
|
writeSupiConfig,
|
|
11
|
-
} from "./config.ts";
|
|
12
|
-
export type { ConfigSettingsHelpers, ConfigSettingsOptions } from "./config-settings.ts";
|
|
13
|
-
export { registerConfigSettings } from "./config-settings.ts";
|
|
14
|
-
export type { ContextMessageLike } from "./context-messages.ts";
|
|
11
|
+
} from "./config/config.ts";
|
|
12
|
+
export type { ConfigSettingsHelpers, ConfigSettingsOptions } from "./config/config-settings.ts";
|
|
13
|
+
export { registerConfigSettings } from "./config/config-settings.ts";
|
|
14
|
+
export type { ContextMessageLike } from "./context/context-messages.ts";
|
|
15
15
|
export {
|
|
16
16
|
findLastUserMessageIndex,
|
|
17
17
|
getContextToken,
|
|
18
18
|
getPromptContent,
|
|
19
19
|
pruneAndReorderContextMessages,
|
|
20
20
|
restorePromptContent,
|
|
21
|
-
} from "./context-messages.ts";
|
|
22
|
-
export type { ContextProvider } from "./context-provider-registry.ts";
|
|
21
|
+
} from "./context/context-messages.ts";
|
|
22
|
+
export type { ContextProvider } from "./context/context-provider-registry.ts";
|
|
23
23
|
export {
|
|
24
24
|
clearRegisteredContextProviders,
|
|
25
25
|
getRegisteredContextProviders,
|
|
26
26
|
registerContextProvider,
|
|
27
|
-
} from "./context-provider-registry.ts";
|
|
28
|
-
export { wrapExtensionContext } from "./context-tag.ts";
|
|
27
|
+
} from "./context/context-provider-registry.ts";
|
|
28
|
+
export { wrapExtensionContext } from "./context/context-tag.ts";
|
|
29
29
|
export type {
|
|
30
30
|
DebugAgentAccess,
|
|
31
31
|
DebugEvent,
|
|
@@ -64,14 +64,14 @@ export {
|
|
|
64
64
|
walkProject,
|
|
65
65
|
} from "./project-roots.ts";
|
|
66
66
|
export { getActiveBranchEntries } from "./session-utils.ts";
|
|
67
|
-
export { registerSettingsCommand } from "./settings-command.ts";
|
|
68
|
-
export type { SettingsScope, SettingsSection } from "./settings-registry.ts";
|
|
67
|
+
export { registerSettingsCommand } from "./settings/settings-command.ts";
|
|
68
|
+
export type { SettingsScope, SettingsSection } from "./settings/settings-registry.ts";
|
|
69
69
|
export {
|
|
70
70
|
clearRegisteredSettings,
|
|
71
71
|
getRegisteredSettings,
|
|
72
72
|
registerSettings,
|
|
73
|
-
} from "./settings-registry.ts";
|
|
74
|
-
export { createInputSubmenu, openSettingsOverlay } from "./settings-ui.ts";
|
|
73
|
+
} from "./settings/settings-registry.ts";
|
|
74
|
+
export { createInputSubmenu, openSettingsOverlay } from "./settings/settings-ui.ts";
|
|
75
75
|
export type { TitleTarget } from "./terminal.ts";
|
|
76
76
|
export {
|
|
77
77
|
DONE_SYMBOL,
|
package/node_modules/@mrclrchtr/supi-core/src/{settings-registry.ts → settings/settings-registry.ts}
RENAMED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// factory function. The generic settings UI reads them via `getRegisteredSettings()`.
|
|
5
5
|
|
|
6
6
|
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
7
|
-
import { createRegistry } from "
|
|
7
|
+
import { createRegistry } from "../registry-utils.ts";
|
|
8
8
|
|
|
9
9
|
export type SettingsScope = "project" | "global";
|
|
10
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-extras",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "SuPi extras — command aliases, skill shorthand, tab spinner, /supi-stash prompt stash with TUI overlay, and other small utilities",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"clipboardy": "^5.3.1",
|
|
24
|
-
"@mrclrchtr/supi-core": "1.
|
|
24
|
+
"@mrclrchtr/supi-core": "1.4.0"
|
|
25
25
|
},
|
|
26
26
|
"bundledDependencies": [
|
|
27
27
|
"@mrclrchtr/supi-core"
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import aliases from "./aliases.ts";
|
|
2
2
|
import copyPrompt from "./copy-prompt.ts";
|
|
3
3
|
import gitEditor from "./git-editor.ts";
|
|
4
|
+
import modelEffortColors from "./model-effort-colors.ts";
|
|
4
5
|
import promptStash from "./prompt-stash.ts";
|
|
5
6
|
import skillShortcut from "./skill-shortcut.ts";
|
|
6
7
|
import tabSpinner from "./tab-spinner.ts";
|
|
@@ -12,4 +13,5 @@ export default function (pi: Parameters<typeof tabSpinner>[0]) {
|
|
|
12
13
|
gitEditor(pi);
|
|
13
14
|
aliases(pi);
|
|
14
15
|
skillShortcut(pi);
|
|
16
|
+
modelEffortColors(pi);
|
|
15
17
|
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for model-effort-colors extension.
|
|
3
|
+
* Extracted to keep the extension entrypoint under complexity / line-count limits.
|
|
4
|
+
*/
|
|
5
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
7
|
+
|
|
8
|
+
// ---- re-exports for the extension entrypoint ----
|
|
9
|
+
|
|
10
|
+
export type ModelInfo = {
|
|
11
|
+
id?: string;
|
|
12
|
+
provider?: string;
|
|
13
|
+
reasoning?: boolean;
|
|
14
|
+
contextWindow?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface UsageEntry {
|
|
18
|
+
type: string;
|
|
19
|
+
message?: {
|
|
20
|
+
role: string;
|
|
21
|
+
usage: {
|
|
22
|
+
input: number;
|
|
23
|
+
output: number;
|
|
24
|
+
cacheRead?: number;
|
|
25
|
+
cacheWrite?: number;
|
|
26
|
+
cost: { total: number };
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UsageTotals {
|
|
32
|
+
totalInput: number;
|
|
33
|
+
totalOutput: number;
|
|
34
|
+
totalCacheRead: number;
|
|
35
|
+
totalCacheWrite: number;
|
|
36
|
+
totalCost: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FooterTheme {
|
|
40
|
+
fg(color: ThemeColor, text: string): string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface FooterData {
|
|
44
|
+
getGitBranch(): string | null;
|
|
45
|
+
getExtensionStatuses(): ReadonlyMap<string, string>;
|
|
46
|
+
onBranchChange(cb: () => void): () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---- color mappings ----
|
|
50
|
+
|
|
51
|
+
const PROVIDER_PATTERNS: Array<[RegExp, ThemeColor]> = [
|
|
52
|
+
[/anthropic|claude/, "accent"],
|
|
53
|
+
[/openai|gpt|chatgpt/, "success"],
|
|
54
|
+
[/google|gemini/, "warning"],
|
|
55
|
+
[/mistral|codestral/, "muted"],
|
|
56
|
+
[/xai|grok/, "thinkingXhigh"],
|
|
57
|
+
[/deepseek/, "thinkingHigh"],
|
|
58
|
+
[/meta|llama/, "thinkingMedium"],
|
|
59
|
+
[/ollama|local/, "dim"],
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
export function providerThemeToken(provider: string | undefined): ThemeColor {
|
|
63
|
+
const haystack = String(provider ?? "").toLowerCase();
|
|
64
|
+
for (const [re, color] of PROVIDER_PATTERNS) {
|
|
65
|
+
if (re.test(haystack)) return color;
|
|
66
|
+
}
|
|
67
|
+
return "dim";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function thinkingThemeToken(level: string): ThemeColor {
|
|
71
|
+
switch (level.toLowerCase()) {
|
|
72
|
+
case "off":
|
|
73
|
+
return "thinkingOff";
|
|
74
|
+
case "minimal":
|
|
75
|
+
return "thinkingMinimal";
|
|
76
|
+
case "low":
|
|
77
|
+
return "thinkingLow";
|
|
78
|
+
case "medium":
|
|
79
|
+
return "thinkingMedium";
|
|
80
|
+
case "high":
|
|
81
|
+
return "thinkingHigh";
|
|
82
|
+
case "xhigh":
|
|
83
|
+
return "thinkingXhigh";
|
|
84
|
+
default:
|
|
85
|
+
return "dim";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- formatting helpers ----
|
|
90
|
+
|
|
91
|
+
export function formatTokens(count: number): string {
|
|
92
|
+
if (count < 1000) return count.toString();
|
|
93
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
94
|
+
if (count < 1_000_000) return `${Math.round(count / 1000)}k`;
|
|
95
|
+
if (count < 10_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
96
|
+
return `${Math.round(count / 1_000_000)}M`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function sanitizeStatusText(text: string): string {
|
|
100
|
+
return text
|
|
101
|
+
.replace(/[\r\n\t]/g, " ")
|
|
102
|
+
.replace(/ +/g, " ")
|
|
103
|
+
.trim();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- usage aggregation ----
|
|
107
|
+
|
|
108
|
+
/** Aggregate token/cost usage across all assistant messages. */
|
|
109
|
+
export function gatherUsage(entries: ReadonlyArray<UsageEntry>): UsageTotals {
|
|
110
|
+
let totalInput = 0;
|
|
111
|
+
let totalOutput = 0;
|
|
112
|
+
let totalCacheRead = 0;
|
|
113
|
+
let totalCacheWrite = 0;
|
|
114
|
+
let totalCost = 0;
|
|
115
|
+
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
if (entry.type === "message" && entry.message?.role === "assistant") {
|
|
118
|
+
const u = entry.message.usage;
|
|
119
|
+
totalInput += u.input;
|
|
120
|
+
totalOutput += u.output;
|
|
121
|
+
totalCacheRead += u.cacheRead ?? 0;
|
|
122
|
+
totalCacheWrite += u.cacheWrite ?? 0;
|
|
123
|
+
totalCost += u.cost.total;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { totalInput, totalOutput, totalCacheRead, totalCacheWrite, totalCost };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---- stats builder ----
|
|
131
|
+
|
|
132
|
+
/** Build the left-side stats string. */
|
|
133
|
+
export function buildStatsLeft(params: {
|
|
134
|
+
contextWindow: number;
|
|
135
|
+
percent: number | null;
|
|
136
|
+
usage: UsageTotals;
|
|
137
|
+
useSubscription: boolean;
|
|
138
|
+
}): { text: string; contextPercentValue: number } {
|
|
139
|
+
const { contextWindow, percent, usage, useSubscription } = params;
|
|
140
|
+
const { totalInput, totalOutput, totalCacheRead, totalCacheWrite, totalCost } = usage;
|
|
141
|
+
|
|
142
|
+
const contextPercentValue = percent ?? 0;
|
|
143
|
+
const contextPercent = percent != null ? percent.toFixed(1) : "?";
|
|
144
|
+
|
|
145
|
+
const parts: string[] = [];
|
|
146
|
+
if (totalInput) parts.push(`↑${formatTokens(totalInput)}`);
|
|
147
|
+
if (totalOutput) parts.push(`↓${formatTokens(totalOutput)}`);
|
|
148
|
+
if (totalCacheRead) parts.push(`R${formatTokens(totalCacheRead)}`);
|
|
149
|
+
if (totalCacheWrite) parts.push(`W${formatTokens(totalCacheWrite)}`);
|
|
150
|
+
|
|
151
|
+
if (totalCost || useSubscription) {
|
|
152
|
+
parts.push(`$${totalCost.toFixed(3)}${useSubscription ? " (sub)" : ""}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const ctxDisplay =
|
|
156
|
+
contextPercent === "?"
|
|
157
|
+
? `?/${formatTokens(contextWindow)}`
|
|
158
|
+
: `${contextPercent}%/${formatTokens(contextWindow)}`;
|
|
159
|
+
parts.push(ctxDisplay);
|
|
160
|
+
|
|
161
|
+
return { text: parts.join(" "), contextPercentValue };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---- context-percent coloring ----
|
|
165
|
+
|
|
166
|
+
/** Color the context-% portion based on thresholds. */
|
|
167
|
+
export function colorContextPercent(statsLeft: string, pctVal: number, theme: FooterTheme): string {
|
|
168
|
+
if (pctVal > 90) {
|
|
169
|
+
return statsLeft.replace(/\d+\.?\d*%/, (m) => theme.fg("error", m));
|
|
170
|
+
}
|
|
171
|
+
if (pctVal > 70) {
|
|
172
|
+
return statsLeft.replace(/\d+\.?\d*%/, (m) => theme.fg("warning", m));
|
|
173
|
+
}
|
|
174
|
+
return statsLeft;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---- right-side builders ----
|
|
178
|
+
|
|
179
|
+
/** Build the styled right portion (model name + effort level). */
|
|
180
|
+
export function styleRightSide(params: {
|
|
181
|
+
model: ModelInfo | undefined;
|
|
182
|
+
thinkingLevel: string;
|
|
183
|
+
theme: FooterTheme;
|
|
184
|
+
statsLeftWidth: number;
|
|
185
|
+
availableWidth: number;
|
|
186
|
+
}): { plain: string; styled: string } {
|
|
187
|
+
const { model, thinkingLevel, theme, statsLeftWidth, availableWidth } = params;
|
|
188
|
+
|
|
189
|
+
const modelName = model?.id ?? "no-model";
|
|
190
|
+
const effortLabel = thinkingLevel === "off" ? "thinking off" : thinkingLevel.toLowerCase();
|
|
191
|
+
|
|
192
|
+
let plain = modelName;
|
|
193
|
+
if (model?.reasoning) {
|
|
194
|
+
plain = `${modelName} • ${effortLabel}`;
|
|
195
|
+
}
|
|
196
|
+
if (model?.provider) {
|
|
197
|
+
const withProvider = `(${model.provider}) ${plain}`;
|
|
198
|
+
if (statsLeftWidth + 2 + visibleWidth(withProvider) <= availableWidth) {
|
|
199
|
+
plain = withProvider;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const providerToken = providerThemeToken(model?.provider);
|
|
204
|
+
const thinkingToken = thinkingThemeToken(thinkingLevel);
|
|
205
|
+
const providerPrefix = model?.provider ? `(${model.provider}) ` : "";
|
|
206
|
+
|
|
207
|
+
let styled = "";
|
|
208
|
+
if (providerPrefix) styled += theme.fg("dim", providerPrefix);
|
|
209
|
+
styled += theme.fg(providerToken, modelName);
|
|
210
|
+
|
|
211
|
+
if (model?.reasoning) {
|
|
212
|
+
styled += theme.fg("dim", " • ");
|
|
213
|
+
styled += theme.fg(thinkingToken, effortLabel);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { plain, styled };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Apply right-justified padding between stats and right side. */
|
|
220
|
+
export function layoutRightSide(params: {
|
|
221
|
+
plain: string;
|
|
222
|
+
styled: string;
|
|
223
|
+
statsLeftWidth: number;
|
|
224
|
+
availableWidth: number;
|
|
225
|
+
}): { styled: string; padding: string } {
|
|
226
|
+
const { plain, styled, statsLeftWidth, availableWidth } = params;
|
|
227
|
+
const rightWidth = visibleWidth(plain);
|
|
228
|
+
const minPadding = 2;
|
|
229
|
+
|
|
230
|
+
if (statsLeftWidth + minPadding + rightWidth <= availableWidth) {
|
|
231
|
+
return { styled, padding: " ".repeat(availableWidth - statsLeftWidth - rightWidth) };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const avail = availableWidth - statsLeftWidth - minPadding;
|
|
235
|
+
if (avail <= 0) {
|
|
236
|
+
return { styled: "", padding: "" };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const finalStyled = truncateToWidth(styled, avail + (styled.length - plain.length), "");
|
|
240
|
+
const finalPlain = truncateToWidth(plain, avail, "");
|
|
241
|
+
const padding = " ".repeat(
|
|
242
|
+
Math.max(0, availableWidth - statsLeftWidth - visibleWidth(finalPlain)),
|
|
243
|
+
);
|
|
244
|
+
return { styled: finalStyled, padding };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---- path line ----
|
|
248
|
+
|
|
249
|
+
/** Build the working-directory line for the footer. */
|
|
250
|
+
export function buildPwdLine(params: {
|
|
251
|
+
cwd: string;
|
|
252
|
+
gitBranch: string | null;
|
|
253
|
+
sessionName: string | undefined;
|
|
254
|
+
}): string {
|
|
255
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
256
|
+
let pwd = params.cwd;
|
|
257
|
+
if (home && pwd.startsWith(home)) {
|
|
258
|
+
pwd = `~${pwd.slice(home.length)}`;
|
|
259
|
+
}
|
|
260
|
+
if (params.gitBranch) pwd = `${pwd} (${params.gitBranch})`;
|
|
261
|
+
if (params.sessionName) pwd = `${pwd} • ${params.sessionName}`;
|
|
262
|
+
return pwd;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---- thinking level ----
|
|
266
|
+
|
|
267
|
+
export function latestThinkingLevel(
|
|
268
|
+
entries: ReadonlyArray<{ type: string; thinkingLevel?: string }>,
|
|
269
|
+
): string {
|
|
270
|
+
let level = "off";
|
|
271
|
+
for (const e of entries) {
|
|
272
|
+
if (e.type === "thinking_level_change" && e.thinkingLevel) {
|
|
273
|
+
level = e.thinkingLevel;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return level;
|
|
277
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model/effort footer coloring for pi.
|
|
3
|
+
*
|
|
4
|
+
* Colors the model name in the footer by semantically mapping the provider
|
|
5
|
+
* to PI theme tokens, and colors the thinking/effort level using PI's
|
|
6
|
+
* built-in thinking-level theme tokens. No hardcoded hex colors, no
|
|
7
|
+
* animations.
|
|
8
|
+
*/
|
|
9
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
11
|
+
import {
|
|
12
|
+
buildPwdLine,
|
|
13
|
+
buildStatsLeft,
|
|
14
|
+
colorContextPercent,
|
|
15
|
+
type FooterData,
|
|
16
|
+
type FooterTheme,
|
|
17
|
+
gatherUsage,
|
|
18
|
+
latestThinkingLevel,
|
|
19
|
+
layoutRightSide,
|
|
20
|
+
type ModelInfo,
|
|
21
|
+
sanitizeStatusText,
|
|
22
|
+
styleRightSide,
|
|
23
|
+
type UsageEntry,
|
|
24
|
+
} from "./model-effort-colors-helpers.ts";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Extension
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export default function modelEffortColors(pi: ExtensionAPI) {
|
|
31
|
+
let currentModel: unknown;
|
|
32
|
+
let requestRender: (() => void) | undefined;
|
|
33
|
+
|
|
34
|
+
pi.on("session_start", (_event, ctx) => {
|
|
35
|
+
currentModel = ctx.model;
|
|
36
|
+
installFooter(ctx);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
pi.on("model_select", (event, _ctx) => {
|
|
40
|
+
currentModel = event.model;
|
|
41
|
+
requestRender?.();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
pi.on("thinking_level_select", (_event, _ctx) => {
|
|
45
|
+
requestRender?.();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
pi.on("session_shutdown", () => {
|
|
49
|
+
currentModel = undefined;
|
|
50
|
+
requestRender = undefined;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ---- footer installation ----
|
|
54
|
+
|
|
55
|
+
// biome-ignore lint/suspicious/noExplicitAny: ctx type from pi session_start handler is complex
|
|
56
|
+
function installFooter(ctx: any) {
|
|
57
|
+
ctx.ui.setFooter(
|
|
58
|
+
(tui: { requestRender(): void }, theme: FooterTheme, footerData: FooterData) => {
|
|
59
|
+
requestRender = () => tui.requestRender();
|
|
60
|
+
const branchUnsub = footerData.onBranchChange(() => tui.requestRender());
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
dispose() {
|
|
64
|
+
branchUnsub();
|
|
65
|
+
requestRender = undefined;
|
|
66
|
+
},
|
|
67
|
+
invalidate() {},
|
|
68
|
+
render(width: number): string[] {
|
|
69
|
+
const model = (currentModel ?? ctx.model) as ModelInfo | undefined;
|
|
70
|
+
|
|
71
|
+
// Thinking level
|
|
72
|
+
const thinkingLevel = resolveThinkingLevel(
|
|
73
|
+
model,
|
|
74
|
+
(pi as { getThinkingLevel?: () => string }).getThinkingLevel,
|
|
75
|
+
ctx.sessionManager.getEntries(),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Usage
|
|
79
|
+
const usage = gatherUsage(ctx.sessionManager.getEntries() as ReadonlyArray<UsageEntry>);
|
|
80
|
+
|
|
81
|
+
// Context window
|
|
82
|
+
const contextUsage = ctx.getContextUsage() as
|
|
83
|
+
| { percent?: number | null; contextWindow?: number }
|
|
84
|
+
| undefined;
|
|
85
|
+
const contextWindow = contextUsage?.contextWindow ?? model?.contextWindow ?? 0;
|
|
86
|
+
const useSubscription = model
|
|
87
|
+
? ctx.modelRegistry.isUsingOAuth(
|
|
88
|
+
// biome-ignore lint/suspicious/noExplicitAny: pi Model<Api> type is generic
|
|
89
|
+
model as any,
|
|
90
|
+
)
|
|
91
|
+
: false;
|
|
92
|
+
|
|
93
|
+
// Stats
|
|
94
|
+
const rawStats = buildStatsLeft({
|
|
95
|
+
contextWindow,
|
|
96
|
+
percent: contextUsage?.percent ?? null,
|
|
97
|
+
usage,
|
|
98
|
+
useSubscription,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
let statsLeft = rawStats.text;
|
|
102
|
+
let statsLeftWidth = visibleWidth(statsLeft);
|
|
103
|
+
if (statsLeftWidth > width) {
|
|
104
|
+
statsLeft = truncateToWidth(statsLeft, width, "...");
|
|
105
|
+
statsLeftWidth = visibleWidth(statsLeft);
|
|
106
|
+
}
|
|
107
|
+
statsLeft = colorContextPercent(statsLeft, rawStats.contextPercentValue, theme);
|
|
108
|
+
|
|
109
|
+
// Right side
|
|
110
|
+
const right = styleRightSide({
|
|
111
|
+
model,
|
|
112
|
+
thinkingLevel,
|
|
113
|
+
theme,
|
|
114
|
+
statsLeftWidth,
|
|
115
|
+
availableWidth: width,
|
|
116
|
+
});
|
|
117
|
+
const laidOut = layoutRightSide({
|
|
118
|
+
plain: right.plain,
|
|
119
|
+
styled: right.styled,
|
|
120
|
+
statsLeftWidth,
|
|
121
|
+
availableWidth: width,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Path line + assembly
|
|
125
|
+
const pwdLine = truncateToWidth(
|
|
126
|
+
theme.fg(
|
|
127
|
+
"dim",
|
|
128
|
+
buildPwdLine({
|
|
129
|
+
cwd: ctx.sessionManager.getCwd(),
|
|
130
|
+
gitBranch: footerData.getGitBranch(),
|
|
131
|
+
sessionName: ctx.sessionManager.getSessionName(),
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
134
|
+
width,
|
|
135
|
+
theme.fg("dim", "..."),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const lines = [
|
|
139
|
+
pwdLine,
|
|
140
|
+
theme.fg("dim", statsLeft) + theme.fg("dim", laidOut.padding) + laidOut.styled,
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// Extension statuses
|
|
144
|
+
buildStatusLine(lines, footerData, width, theme);
|
|
145
|
+
|
|
146
|
+
return lines;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Resolve the current thinking level. */
|
|
155
|
+
function resolveThinkingLevel(
|
|
156
|
+
model: ModelInfo | undefined,
|
|
157
|
+
getThinkingLevel: (() => string) | undefined,
|
|
158
|
+
entries: ReadonlyArray<{ type: string; thinkingLevel?: string }>,
|
|
159
|
+
): string {
|
|
160
|
+
if (!model?.reasoning) return "off";
|
|
161
|
+
return getThinkingLevel?.() ?? latestThinkingLevel(entries);
|
|
162
|
+
}
|
|
163
|
+
function buildStatusLine(
|
|
164
|
+
lines: string[],
|
|
165
|
+
footerData: FooterData,
|
|
166
|
+
width: number,
|
|
167
|
+
theme: FooterTheme,
|
|
168
|
+
): void {
|
|
169
|
+
const statuses = footerData.getExtensionStatuses();
|
|
170
|
+
if (statuses.size === 0) return;
|
|
171
|
+
|
|
172
|
+
const entries = Array.from(statuses.entries()) as Array<[string, string]>;
|
|
173
|
+
const statusLine = entries
|
|
174
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
175
|
+
.map(([, text]) => sanitizeStatusText(text))
|
|
176
|
+
.join(" ");
|
|
177
|
+
lines.push(truncateToWidth(statusLine, width, theme.fg("dim", "...")));
|
|
178
|
+
}
|
package/src/tab-spinner.ts
CHANGED
|
@@ -13,12 +13,15 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
13
13
|
import { formatTitle, signalDone } from "@mrclrchtr/supi-core/api";
|
|
14
14
|
|
|
15
15
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
16
|
+
const AGENT_END_SETTLE_MS = 200;
|
|
16
17
|
|
|
17
18
|
export default function tabSpinner(pi: ExtensionAPI) {
|
|
18
19
|
let timer: ReturnType<typeof setInterval> | null = null;
|
|
20
|
+
let pendingAgentEndTimer: ReturnType<typeof setTimeout> | null = null;
|
|
19
21
|
let frame = 0;
|
|
20
22
|
let activeCount = 0;
|
|
21
23
|
let hasActiveAgent = false;
|
|
24
|
+
let pendingAgentEnd = false;
|
|
22
25
|
let askUserActive = 0;
|
|
23
26
|
let currentCtx: ExtensionContext | undefined;
|
|
24
27
|
|
|
@@ -27,28 +30,34 @@ export default function tabSpinner(pi: ExtensionAPI) {
|
|
|
27
30
|
return formatTitle(pi.getSessionName(), currentCtx?.cwd);
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
function clearPendingAgentEnd() {
|
|
34
|
+
pendingAgentEnd = false;
|
|
35
|
+
if (pendingAgentEndTimer) {
|
|
36
|
+
clearTimeout(pendingAgentEndTimer);
|
|
37
|
+
pendingAgentEndTimer = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
/** Restore the base title immediately. */
|
|
31
42
|
function stop() {
|
|
43
|
+
clearPendingAgentEnd();
|
|
32
44
|
if (timer) {
|
|
33
45
|
clearInterval(timer);
|
|
34
46
|
timer = null;
|
|
35
47
|
}
|
|
36
48
|
frame = 0;
|
|
37
|
-
if (currentCtx)
|
|
38
|
-
currentCtx.ui.setTitle(title());
|
|
39
|
-
}
|
|
49
|
+
if (currentCtx) currentCtx.ui.setTitle(title());
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
/** Show the ✓ done symbol in the title and play the terminal bell. */
|
|
43
53
|
function showDone() {
|
|
54
|
+
clearPendingAgentEnd();
|
|
44
55
|
if (timer) {
|
|
45
56
|
clearInterval(timer);
|
|
46
57
|
timer = null;
|
|
47
58
|
}
|
|
48
59
|
frame = 0;
|
|
49
|
-
if (currentCtx)
|
|
50
|
-
signalDone(currentCtx, title());
|
|
51
|
-
}
|
|
60
|
+
if (currentCtx) signalDone(currentCtx, title());
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
/** Start the spinner interval. Overwrites any ✓ shown. */
|
|
@@ -63,30 +72,63 @@ export default function tabSpinner(pi: ExtensionAPI) {
|
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
function increment(ctx: ExtensionContext) {
|
|
75
|
+
clearPendingAgentEnd();
|
|
66
76
|
currentCtx = ctx;
|
|
67
77
|
activeCount++;
|
|
68
|
-
if (activeCount === 1) start();
|
|
78
|
+
if (activeCount === 1 && askUserActive === 0) start();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resumePendingAgent(ctx: ExtensionContext) {
|
|
82
|
+
if (!pendingAgentEnd) return;
|
|
83
|
+
clearPendingAgentEnd();
|
|
84
|
+
currentCtx = ctx;
|
|
85
|
+
hasActiveAgent = true;
|
|
86
|
+
if (askUserActive === 0) start();
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
/** Decrement count for supi:working tasks — restores title when idle. */
|
|
72
90
|
function decrement() {
|
|
73
|
-
const floor = hasActiveAgent ? 1 : 0;
|
|
91
|
+
const floor = hasActiveAgent || pendingAgentEnd ? 1 : 0;
|
|
74
92
|
activeCount = Math.max(floor, activeCount - 1);
|
|
75
93
|
if (activeCount === 0) stop();
|
|
76
94
|
}
|
|
77
95
|
|
|
78
|
-
|
|
79
|
-
|
|
96
|
+
function finalizeAgentEnd() {
|
|
97
|
+
clearPendingAgentEnd();
|
|
80
98
|
activeCount = Math.max(0, activeCount - 1);
|
|
81
|
-
if (activeCount === 0)
|
|
99
|
+
if (activeCount === 0) {
|
|
100
|
+
showDone();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (askUserActive === 0) start();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Defer the done state briefly so immediate retries do not flash ✓. */
|
|
107
|
+
function agentEnded() {
|
|
108
|
+
clearPendingAgentEnd();
|
|
109
|
+
pendingAgentEnd = true;
|
|
110
|
+
pendingAgentEndTimer = setTimeout(() => {
|
|
111
|
+
finalizeAgentEnd();
|
|
112
|
+
}, AGENT_END_SETTLE_MS);
|
|
113
|
+
pendingAgentEndTimer.unref?.();
|
|
82
114
|
}
|
|
83
115
|
|
|
84
116
|
pi.on("agent_start", async (_event, ctx) => {
|
|
117
|
+
if (pendingAgentEnd) {
|
|
118
|
+
resumePendingAgent(ctx);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
85
121
|
hasActiveAgent = true;
|
|
86
122
|
increment(ctx);
|
|
87
123
|
});
|
|
88
124
|
|
|
89
|
-
pi.on("
|
|
125
|
+
pi.on("turn_start", async (_event, ctx) => resumePendingAgent(ctx));
|
|
126
|
+
|
|
127
|
+
pi.on("agent_end", async (event, _ctx) => {
|
|
128
|
+
const retryAwareEvent = event as { willRetry?: boolean };
|
|
129
|
+
// Extension events do not currently type `willRetry`, but honor it when
|
|
130
|
+
// present and otherwise rely on the short settle window plus turn_start.
|
|
131
|
+
if (retryAwareEvent.willRetry) return;
|
|
90
132
|
hasActiveAgent = false;
|
|
91
133
|
agentEnded();
|
|
92
134
|
});
|
|
@@ -100,10 +142,7 @@ export default function tabSpinner(pi: ExtensionAPI) {
|
|
|
100
142
|
pi.events.on("supi:working:start", () => {
|
|
101
143
|
if (currentCtx) increment(currentCtx);
|
|
102
144
|
});
|
|
103
|
-
|
|
104
|
-
pi.events.on("supi:working:end", () => {
|
|
105
|
-
decrement();
|
|
106
|
-
});
|
|
145
|
+
pi.events.on("supi:working:end", () => decrement());
|
|
107
146
|
|
|
108
147
|
pi.events.on("supi:ask-user:start", () => {
|
|
109
148
|
askUserActive++;
|
|
@@ -117,9 +156,7 @@ export default function tabSpinner(pi: ExtensionAPI) {
|
|
|
117
156
|
|
|
118
157
|
pi.events.on("supi:ask-user:end", () => {
|
|
119
158
|
askUserActive = Math.max(0, askUserActive - 1);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
start();
|
|
123
|
-
}
|
|
159
|
+
// Resume the spinner if the agent (or background work) is still running.
|
|
160
|
+
if (askUserActive === 0 && activeCount > 0) start();
|
|
124
161
|
});
|
|
125
162
|
}
|
|
File without changes
|
/package/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts}
RENAMED
|
File without changes
|
|
File without changes
|
/package/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts}
RENAMED
|
File without changes
|
|
File without changes
|