@maci0/dsh-ponytail 0.0.0-stage → 0.18.2
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/LICENSE +22 -0
- package/README.md +184 -2
- package/cordis.patch.yml +15 -0
- package/icon.svg +5 -0
- package/lib/client.js +376 -0
- package/lib/frontmatter.js +285 -0
- package/lib/host.js +12 -0
- package/lib/index.js +375 -0
- package/lib/modes.js +158 -0
- package/lib/skills.js +209 -0
- package/lib/types/frontmatter.d.ts +45 -0
- package/lib/types/host.d.ts +159 -0
- package/lib/types/index.d.ts +56 -0
- package/lib/types/modes.d.ts +75 -0
- package/lib/types/skills.d.ts +58 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +112 -4
- package/skills/ponytail/SKILL.md +124 -0
- package/skills/ponytail-audit/SKILL.md +41 -0
- package/skills/ponytail-debt/SKILL.md +44 -0
- package/skills/ponytail-gain/SKILL.md +49 -0
- package/skills/ponytail-help/SKILL.md +65 -0
- package/skills/ponytail-review/SKILL.md +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-ponytail contributors
|
|
4
|
+
Copyright (c) 2026 DietrichGebert (https://github.com/DietrichGebert/ponytail)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,185 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-ponytail
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Your agent adds a cache class, a config flag, and a factory for one product. This
|
|
4
|
+
plugin makes it stop and write the one line that works instead. The ladder:
|
|
5
|
+
question whether the thing needs to exist, reuse what is already in the repo, then
|
|
6
|
+
the standard library, then the platform, then an installed dependency, then one
|
|
7
|
+
line. Stop at the first rung that holds. Adapted from
|
|
8
|
+
[DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail) (MIT): "He
|
|
9
|
+
says nothing. He writes one line. It works."
|
|
10
|
+
|
|
11
|
+
## What you get
|
|
12
|
+
|
|
13
|
+
- **The ladder on every request.** While any level except `off` is active, the
|
|
14
|
+
mode-filtered ruleset joins the system prompt.
|
|
15
|
+
- **Six skills** the `skill` tool can load, so they also appear as
|
|
16
|
+
`/ponytail-review`, `/ponytail-audit`, … in the composer: `ponytail`,
|
|
17
|
+
`ponytail-review`, `ponytail-audit`, `ponytail-debt`, `ponytail-gain`,
|
|
18
|
+
`ponytail-help`.
|
|
19
|
+
- **Two ways to switch level.** The model calls the `ponytail` tool; you type
|
|
20
|
+
`/ponytail <level>` or just say **stop ponytail**.
|
|
21
|
+
- **A settings card and a composer chip**, so the level is visible without opening Settings.
|
|
22
|
+
- **No unrequested abstractions, no boilerplate.** Deletion over addition, boring
|
|
23
|
+
over clever.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
> **Install it as a bundle.** `dsh plugin add …` mounts the row from the
|
|
28
|
+
> package's own patch layer, which is what the settings editor can write to. A
|
|
29
|
+
> row added with `--patch` is an overlay: it disappears at the next start, and
|
|
30
|
+
> the Plugins card cannot save into it (the editor refuses a write an overlay
|
|
31
|
+
> would win).
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
dsh plugin --profile web add @maci0/dsh-ponytail@0.18.2
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This installs the public npm package; no GitHub token or `~/.secrets` setup is needed.
|
|
38
|
+
The version is pinned. To upgrade, run the same command with a newer version,
|
|
39
|
+
then restart `dsh web` (bundle layers compose at boot).
|
|
40
|
+
|
|
41
|
+
## Use it
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
/ponytail ultra -> Ponytail level: ultra (was full).
|
|
45
|
+
/ponytail -> Ponytail level: ultra.
|
|
46
|
+
/ponytail off -> Ponytail off (was ultra). Normal behavior.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then ask for the work. This is the `full` example from the skill itself:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
> Add a cache for these API responses.
|
|
53
|
+
|
|
54
|
+
`@lru_cache(maxsize=1000)` on the fetch function. Skipped custom cache class,
|
|
55
|
+
add when lru_cache measurably falls short.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Model side, the tool takes one optional argument: `{"mode": "lite"}` sets and
|
|
59
|
+
persists a level, and calling it with no argument reports the current one.
|
|
60
|
+
|
|
61
|
+
Typing **stop ponytail** or **normal mode** as an ordinary message has the same
|
|
62
|
+
effect as `/ponytail off`, and it lands on the turn that carried it. Only the
|
|
63
|
+
human's own words count: injected context riding the same event stream cannot
|
|
64
|
+
toggle the level, and the message must *be* the command, so "add a normal mode
|
|
65
|
+
toggle" is left alone.
|
|
66
|
+
|
|
67
|
+
### Levels
|
|
68
|
+
|
|
69
|
+
| Level | Behavior | Persisted |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `lite` | Build what was asked, name the lazier alternative in one line. | yes |
|
|
72
|
+
| `full` | The ladder enforced. Default. | yes |
|
|
73
|
+
| `ultra` | YAGNI extremist: challenge the requirement before building it. | yes |
|
|
74
|
+
| `review` | Over-engineering review; points at the `ponytail-review` skill. | no, session only |
|
|
75
|
+
| `off` | No injection. Normal behavior. | yes |
|
|
76
|
+
|
|
77
|
+
The four persistable levels live in the `ponytail` settings namespace, so the
|
|
78
|
+
card, the chip, the tool, and `/ponytail` agree and the choice survives a
|
|
79
|
+
restart. `review` stays session-only because it is a review mode, not a level a
|
|
80
|
+
deployment should start in; the tool and the command still accept it. The card
|
|
81
|
+
and the chip show the level the host is using, so they show `review`, and any level
|
|
82
|
+
kept only for this session because the settings write failed, labelled as
|
|
83
|
+
session-only.
|
|
84
|
+
|
|
85
|
+
Lazy is not negligent. Understanding the problem, trust-boundary validation,
|
|
86
|
+
data-loss handling, security, and accessibility are never on the chopping block.
|
|
87
|
+
|
|
88
|
+
## Configure
|
|
89
|
+
|
|
90
|
+
| Field | Default | Meaning |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `defaultMode` | `full` | Startup level. Must be `off`, `lite`, `full`, or `ultra`. |
|
|
93
|
+
|
|
94
|
+
The exported `Config` schema defaults the field, so a row that omits `defaultMode`
|
|
95
|
+
starts in `full`. The user's settings namespace overrides it once a level is chosen
|
|
96
|
+
in the card or by `/ponytail`. An invalid level fails while the plugin loads rather
|
|
97
|
+
than quietly doing the wrong thing: the loader validates the row against the
|
|
98
|
+
exported schema.
|
|
99
|
+
|
|
100
|
+
To set the startup level from the environment, override the row in your profile's
|
|
101
|
+
`cordis.patch.yml` with a `!!js` expression (same `id`, which replaces the row's
|
|
102
|
+
whole `config`):
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
- id: ponytail
|
|
106
|
+
config:
|
|
107
|
+
defaultMode: !!js process.env.PONYTAIL_DEFAULT_MODE ?? 'full'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Override the row this way; do **not** paste the bundle's `insert` of the
|
|
111
|
+
`id: ponytail` row into your profile's `cordis.patch.yml`: `insert` does not
|
|
112
|
+
dedupe ids, and a second row mounts the plugin twice.
|
|
113
|
+
|
|
114
|
+
## How it works
|
|
115
|
+
|
|
116
|
+
The package declares `dsh.bundle`, so `dsh plugin add` appends it to
|
|
117
|
+
`dsh.profile.bundles` and the row in its own `cordis.patch.yml` applies as a layer.
|
|
118
|
+
|
|
119
|
+
The host half mounts through public Cordis extension points: `systemPrompt.section`,
|
|
120
|
+
`skills.registerProvider`, `tools.register`, `commands.register`,
|
|
121
|
+
`loader/volatile-update` (the settings document writes the row's volatile
|
|
122
|
+
`defaultMode`), `session/event` for the message switch, and `webServer` for
|
|
123
|
+
`GET /ponytail/level`, which answers `{ mode, source }` (`settings` or `session`)
|
|
124
|
+
behind the `connection` trust fence. The browser half reads that route on every
|
|
125
|
+
settings change and every 5 seconds, draws its card into the public `plugins.row.config` slot from `configForms`, registers its
|
|
126
|
+
copy through `locale.register`, and draws its chip into `conversation.input.left`, so
|
|
127
|
+
this plugin needs no client change of its own.
|
|
128
|
+
|
|
129
|
+
The entry point is the built `lib/index.js` (declarations in `lib/types/`); `bun run
|
|
130
|
+
build` regenerates it from `src/`. `lib/client.js` is hand-authored plain JavaScript:
|
|
131
|
+
the client module system serves it as a lazy-CJS factory on `window.__ModuleLoader__`
|
|
132
|
+
because the package exports `./client`, and it is not built. Skills come from
|
|
133
|
+
`skills/<name>/SKILL.md` with frontmatter parsed by `yaml`; the provider takes its rank
|
|
134
|
+
and name grammar from `@deepseek-ai/dsh-skill`, projects `disable-model-invocation`,
|
|
135
|
+
`user-invocable`, and `whenToUse`, and settles on the lookup's abort signal. Tools use
|
|
136
|
+
`defineTool` from `@deepseek-ai/dsh-tools` and forward `exec.signal`. Only
|
|
137
|
+
`skills/ponytail/SKILL.md` is the source of truth for the ruleset; this README keeps no
|
|
138
|
+
second copy.
|
|
139
|
+
|
|
140
|
+
## Limits
|
|
141
|
+
|
|
142
|
+
- **It does not skip comprehension.** The ladder shortens the solution, never the
|
|
143
|
+
reading. The skill says so in its own text.
|
|
144
|
+
- **The level is process-wide.** One prompt section, one namespace value: every
|
|
145
|
+
agent in the process shares it.
|
|
146
|
+
- **External subagents ignore it.** In-process children inherit the ruleset, but
|
|
147
|
+
`subagent-claude-code` and `subagent-codex` spawn their own CLI with its own
|
|
148
|
+
system prompt, and no harness extension point wraps a spawn.
|
|
149
|
+
- **Two locales.** The card and the chip ship `en` and `zh`; any other locale
|
|
150
|
+
falls back through the service's own chain.
|
|
151
|
+
- **Host source edits need `bun run build` and a restart**, because the Loader
|
|
152
|
+
loads `lib/index.js` and a bundle layer composes at boot.
|
|
153
|
+
- **Browser-half edits need a page refresh.**
|
|
154
|
+
- **A level change is not a session event.** A replayed session shows the ruleset
|
|
155
|
+
each request already carried.
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
bun install # the client packages are optional peers and stay uninstalled
|
|
161
|
+
bun run build # tsc -p tsconfig.build.json -> lib/index.js + lib/types/
|
|
162
|
+
bun test # every tests/*.test.ts, no build step
|
|
163
|
+
bun run typecheck # tsc -p tsconfig.json
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
dsh loads plugins on Node ^22.19.0 || >=24.0.0; development and tests run on bun.
|
|
167
|
+
|
|
168
|
+
For local development, install the checkout into a profile with
|
|
169
|
+
`dsh plugin --profile <name> add <path-to-checkout>`.
|
|
170
|
+
|
|
171
|
+
The suite covers level normalization and filtering, the fake-host surface, the skills
|
|
172
|
+
provider, the client card, and a real Cordis composition mount next to the real skill
|
|
173
|
+
registry. To uninstall, run `dsh plugin --profile web remove @maci0/dsh-ponytail`; that removes
|
|
174
|
+
the dependency and the bundle layer together.
|
|
175
|
+
|
|
176
|
+
## Attribution and license
|
|
177
|
+
|
|
178
|
+
MIT. Skill content and mode semantics: © DietrichGebert
|
|
179
|
+
([ponytail](https://github.com/DietrichGebert/ponytail)). DSH port: see `LICENSE`.
|
|
180
|
+
|
|
181
|
+
The six `skills/*/SKILL.md` files are verbatim copies, so they carry upstream's figures
|
|
182
|
+
and citations rather than this package's. `/ponytail-gain` therefore reports the original
|
|
183
|
+
five-task single-shot benchmark (80–94% fewer lines, 47–77% cheaper, 3–6× faster),
|
|
184
|
+
which upstream's own README has since revised to roughly 54% fewer lines, 20% cheaper
|
|
185
|
+
and 27% faster on its agentic benchmark.
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# The dsh-ponytail bundle patch: applied automatically when a profile lists
|
|
2
|
+
# this bundle (`dsh plugin add`/`update` appends the package to
|
|
3
|
+
# dsh.profile.bundles). Users override this row from their profile's own
|
|
4
|
+
# cordis.patch.yml (live-watched; dsh.profile.bundles is frozen at boot) with a
|
|
5
|
+
# `- id: ponytail` row, which replaces the row's whole `config`.
|
|
6
|
+
# `name` stays a bare package specifier: the browser half is served by the
|
|
7
|
+
# client module system, which resolves the Loader entry's package, reads its
|
|
8
|
+
# `dsh.client` manifest, and serves `exports["./client"]`.
|
|
9
|
+
# Do not also insert this same row into the profile patch: insert does not
|
|
10
|
+
# dedupe ids, and a second row would register the plugin twice.
|
|
11
|
+
- insert:
|
|
12
|
+
- id: ponytail
|
|
13
|
+
name: '@maci0/dsh-ponytail'
|
|
14
|
+
config:
|
|
15
|
+
defaultMode: full
|
package/icon.svg
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<circle cx="12" cy="11.5" r="4.2" stroke="#0F6E56" stroke-width="2.2"/>
|
|
3
|
+
<circle cx="12" cy="24.5" r="4.2" stroke="#0F6E56" stroke-width="2.2"/>
|
|
4
|
+
<path d="M15.4 13.8L28 25.5M15.4 22.2L28 10.5" stroke="#1F8A70" stroke-width="2.2" stroke-linecap="round"/>
|
|
5
|
+
</svg>
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-ponytail browser half.
|
|
3
|
+
*
|
|
4
|
+
* Two surfaces read one settings namespace:
|
|
5
|
+
*
|
|
6
|
+
* - the Ponytail card on the Plugins page, keyed on the
|
|
7
|
+
* `ponytail` namespace the host half registers; and
|
|
8
|
+
* - a read-only level chip in the composer tool row (`conversation.input.left`),
|
|
9
|
+
* so the active level is visible without opening Settings.
|
|
10
|
+
*
|
|
11
|
+
* Both read `ctx.configForms`, so they cannot disagree, and both take their
|
|
12
|
+
* copy from the `ponytail` locale namespace registered here (en/zh), which is
|
|
13
|
+
* also why every label has an English and a Chinese entry.
|
|
14
|
+
*
|
|
15
|
+
* Chrome is a stylesheet, not inline style objects: the module system claims
|
|
16
|
+
* every `<style>` tag a factory appends while it materializes and removes it
|
|
17
|
+
* when the package unloads, so the tags cost nothing to own. It also keeps
|
|
18
|
+
* state changes out of React's inline-style diffing, which is what silently
|
|
19
|
+
* blanked a deselected pill's border (a longhand removed against a set
|
|
20
|
+
* shorthand decomposes the shorthand).
|
|
21
|
+
*
|
|
22
|
+
* This file is plain JavaScript on purpose. The client module system serves a
|
|
23
|
+
* package's `exports["./client"]` artifact as a lazy-CJS factory registered on
|
|
24
|
+
* `window.__ModuleLoader__`, and that is the whole format: an out-of-tree
|
|
25
|
+
* plugin can author it directly instead of reproducing the repository's tsdown
|
|
26
|
+
* client preset. `react` is provided by the module system; nothing else is
|
|
27
|
+
* required here.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
window.__ModuleLoader__.load({
|
|
31
|
+
id: '@maci0/dsh-ponytail',
|
|
32
|
+
|
|
33
|
+
factory: (require) => {
|
|
34
|
+
var module = { exports: {} }
|
|
35
|
+
var exports = module.exports
|
|
36
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
37
|
+
|
|
38
|
+
const React = require('react')
|
|
39
|
+
|
|
40
|
+
/** Settings namespace shared with the host half; also this card's slot key. */
|
|
41
|
+
const NAMESPACE = 'ponytail'
|
|
42
|
+
|
|
43
|
+
/** Locale namespace for this plugin's copy: both surfaces read one dictionary. */
|
|
44
|
+
const LOCALE_NS = 'ponytail'
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The host half's route reporting the level in use and its source. Same
|
|
48
|
+
* origin, so the session cookie rides along.
|
|
49
|
+
*/
|
|
50
|
+
const LEVEL_ROUTE = '/ponytail/level'
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* How often a mounted surface re-reads the route. A session-local level
|
|
54
|
+
* changes without a settings write, so no snapshot change announces it.
|
|
55
|
+
*/
|
|
56
|
+
const LEVEL_POLL_MS = 5000
|
|
57
|
+
|
|
58
|
+
/** Every class is `dp-`-prefixed: the sheet lands in the page's own document. */
|
|
59
|
+
const CSS = [
|
|
60
|
+
'.dp-page{display:flex;flex-direction:column;gap:10px}',
|
|
61
|
+
'.dp-body{border-top:0.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0 8px;display:flex;flex-direction:column;gap:10px}',
|
|
62
|
+
'.dp-row{display:flex;flex-wrap:wrap;gap:8px}',
|
|
63
|
+
'.dp-pill{appearance:none;font:inherit;font-size:13px;line-height:1.5;padding:5px 14px;cursor:pointer;color:var(--dsw-alias-label-secondary);background:none;border:1px solid var(--dsw-alias-border-l2);border-radius:999px}',
|
|
64
|
+
'.dp-pill-selected{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-4)}',
|
|
65
|
+
'.dp-pill:disabled{cursor:default;opacity:.5}',
|
|
66
|
+
'.dp-hint{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}',
|
|
67
|
+
'.dp-status{display:flex;align-items:center;gap:8px;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}',
|
|
68
|
+
'.dp-reset{appearance:none;font:inherit;font-size:12px;line-height:1.5;padding:3px 10px;cursor:pointer;color:var(--dsw-alias-label-secondary);background:none;border:1px solid var(--dsw-alias-border-l2);border-radius:8px}',
|
|
69
|
+
'.dp-error{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}',
|
|
70
|
+
'.dp-chip{display:inline-flex;align-items:center;height:24px;padding:0 10px;max-width:180px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:12px;line-height:1.4;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-module-platform);border:1px solid var(--dsw-alias-border-l2);border-radius:999px}',
|
|
71
|
+
].join('')
|
|
72
|
+
|
|
73
|
+
// Appended while the factory materializes: the module system claims the tag
|
|
74
|
+
// for this package and disposes it on unload. Guarded because the node unit
|
|
75
|
+
// tests evaluate this file without a DOM.
|
|
76
|
+
if (typeof document !== 'undefined') {
|
|
77
|
+
const style = document.createElement('style')
|
|
78
|
+
style.textContent = CSS
|
|
79
|
+
document.head.append(style)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const en = {
|
|
83
|
+
description: 'Lazy senior dev mode, level: {level}{note}.',
|
|
84
|
+
overridden: ' (overridden)',
|
|
85
|
+
fromSession: ' (this session only)',
|
|
86
|
+
sourceSession: 'Active for this session only; it is not saved in your settings.',
|
|
87
|
+
levelReview: 'Review',
|
|
88
|
+
hintReview: 'Over-engineering review: the model loads the ponytail-review skill.',
|
|
89
|
+
levelGroup: 'Ponytail level',
|
|
90
|
+
indicator: 'Ponytail: {level}',
|
|
91
|
+
levelOff: 'Off',
|
|
92
|
+
levelLite: 'Lite',
|
|
93
|
+
levelFull: 'Full',
|
|
94
|
+
levelUltra: 'Ultra',
|
|
95
|
+
hintOff: 'No ruleset injected. Normal behavior.',
|
|
96
|
+
hintLite: 'Build what was asked, and name the lazier alternative in one line.',
|
|
97
|
+
hintFull: 'The ladder enforced: YAGNI, reuse, stdlib, native platform, one line, then the minimum that works.',
|
|
98
|
+
hintUltra: 'Challenge whether the requirement needs to exist before building it.',
|
|
99
|
+
persists: 'Applies to every request and persists in your settings.',
|
|
100
|
+
readOnly: 'Read-only: settings are not persisted in this deployment.',
|
|
101
|
+
reset: 'Reset',
|
|
102
|
+
refused: 'The Host refused the change. Try again.',
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const zh = {
|
|
106
|
+
description: '懒人资深开发模式,级别:{level}{note}。',
|
|
107
|
+
overridden: '(已覆盖)',
|
|
108
|
+
fromSession: '(仅本会话)',
|
|
109
|
+
sourceSession: '仅在本会话生效,不会保存到你的设置中。',
|
|
110
|
+
levelReview: '审查',
|
|
111
|
+
hintReview: '过度设计审查:模型加载 ponytail-review 技能。',
|
|
112
|
+
levelGroup: 'Ponytail 级别',
|
|
113
|
+
indicator: 'Ponytail:{level}',
|
|
114
|
+
levelOff: '关闭',
|
|
115
|
+
levelLite: '轻量',
|
|
116
|
+
levelFull: '完整',
|
|
117
|
+
levelUltra: '极端',
|
|
118
|
+
hintOff: '不注入任何规则集,行为如常。',
|
|
119
|
+
hintLite: '按要求实现,并用一行指出更省的做法。',
|
|
120
|
+
hintFull: '强制执行阶梯:YAGNI、复用、标准库、平台原生、已装依赖、一行,然后才是能用的最少代码。',
|
|
121
|
+
hintUltra: '动手之前先质疑这个需求是否必须存在。',
|
|
122
|
+
persists: '对每个请求生效,并保存在你的设置中。',
|
|
123
|
+
readOnly: '只读:此部署不会持久化设置。',
|
|
124
|
+
reset: '重置',
|
|
125
|
+
refused: 'Host 拒绝了更改,请重试。',
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Levels a user may persist. `review` is session-only and lives on the command. */
|
|
129
|
+
const LEVELS = [
|
|
130
|
+
{ value: 'off', label: 'levelOff', hint: 'hintOff' },
|
|
131
|
+
{ value: 'lite', label: 'levelLite', hint: 'hintLite' },
|
|
132
|
+
{ value: 'full', label: 'levelFull', hint: 'hintFull' },
|
|
133
|
+
{ value: 'ultra', label: 'levelUltra', hint: 'hintUltra' },
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
/** Every level the host may report: the persisted ones plus session-only `review`. */
|
|
137
|
+
const KNOWN_LEVELS = [...LEVELS, { value: 'review', label: 'levelReview', hint: 'hintReview' }]
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Localized name of a level, falling back to the raw value for one this card
|
|
141
|
+
* does not offer (a hand-edited document, a future level).
|
|
142
|
+
* @param t - translate function bound to this plugin's namespace.
|
|
143
|
+
* @param mode - level value.
|
|
144
|
+
* @returns the display name.
|
|
145
|
+
*/
|
|
146
|
+
function levelLabel(t, mode) {
|
|
147
|
+
const level = KNOWN_LEVELS.filter((candidate) => candidate.value === mode)[0]
|
|
148
|
+
return level === undefined ? mode : t(level.label)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Bind one scope to a React subscription.
|
|
153
|
+
* @param scope - a scope bound to the ponytail settings namespace.
|
|
154
|
+
* @returns a hook reading that scope's current snapshot.
|
|
155
|
+
*/
|
|
156
|
+
function useScope(scope) {
|
|
157
|
+
const subscribe = (listener) => scope.subscribe(listener)
|
|
158
|
+
const getSnapshot = () => scope.getSnapshot()
|
|
159
|
+
return () => React.useSyncExternalStore(subscribe, getSnapshot)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Read a snapshot's level. A namespace the deployment does not serve reports
|
|
164
|
+
* no value, which every surface renders as nothing.
|
|
165
|
+
* @param snapshot - the settings scope snapshot.
|
|
166
|
+
* @returns the active level value, or `undefined` when unreadable.
|
|
167
|
+
*/
|
|
168
|
+
function modeOf(snapshot) {
|
|
169
|
+
if (snapshot.status !== 'ready') return undefined
|
|
170
|
+
const value = snapshot.value !== null && typeof snapshot.value === 'object' ? snapshot.value : {}
|
|
171
|
+
return typeof value.defaultMode === 'string' ? value.defaultMode : 'full'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Read the level the host half is using, re-reading on every settings
|
|
176
|
+
* change and on a timer. `null` until the route answers, and whenever it
|
|
177
|
+
* cannot (an older host half, a rejected request): the surfaces then fall
|
|
178
|
+
* back to the settings document.
|
|
179
|
+
* @param snapshot - the settings scope snapshot; a new one triggers a read.
|
|
180
|
+
* @returns `{ mode, source }` from the host, or `null`.
|
|
181
|
+
*/
|
|
182
|
+
function useHostLevel(snapshot) {
|
|
183
|
+
const [level, setLevel] = React.useState(null)
|
|
184
|
+
React.useEffect(() => {
|
|
185
|
+
let live = true
|
|
186
|
+
const controller = new AbortController()
|
|
187
|
+
const read = () => {
|
|
188
|
+
fetch(LEVEL_ROUTE, { headers: { accept: 'application/json' }, signal: controller.signal })
|
|
189
|
+
.then((response) => (response.ok ? response.json() : null))
|
|
190
|
+
.then((body) => { if (live) setLevel(hostLevelOf(body)) })
|
|
191
|
+
.catch(() => { if (live) setLevel(null) })
|
|
192
|
+
}
|
|
193
|
+
read()
|
|
194
|
+
const timer = setInterval(read, LEVEL_POLL_MS)
|
|
195
|
+
return () => {
|
|
196
|
+
live = false
|
|
197
|
+
controller.abort()
|
|
198
|
+
clearInterval(timer)
|
|
199
|
+
}
|
|
200
|
+
}, [snapshot])
|
|
201
|
+
return level
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Validate the route's answer: a JSON body is input from the wire.
|
|
206
|
+
* @param body - the parsed response.
|
|
207
|
+
* @returns `{ mode, source }` when both are strings, else `null`.
|
|
208
|
+
*/
|
|
209
|
+
function hostLevelOf(body) {
|
|
210
|
+
if (body === null || typeof body !== 'object') return null
|
|
211
|
+
if (typeof body.mode !== 'string' || typeof body.source !== 'string') return null
|
|
212
|
+
return { mode: body.mode, source: body.source }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The level a surface shows: the host's when it answered, else the
|
|
217
|
+
* settings document's. `undefined` while the namespace is not served.
|
|
218
|
+
* @param snapshot - the settings scope snapshot.
|
|
219
|
+
* @param host - the route's answer, or `null`.
|
|
220
|
+
* @returns the level value, or `undefined`.
|
|
221
|
+
*/
|
|
222
|
+
function shownMode(snapshot, host) {
|
|
223
|
+
const documented = modeOf(snapshot)
|
|
224
|
+
if (documented === undefined) return undefined
|
|
225
|
+
return host === null ? documented : host.mode
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Build the card component over one bound settings scope.
|
|
230
|
+
* @param scope - the scope bound to the ponytail namespace.
|
|
231
|
+
* @param t - translate function bound to this plugin's locale namespace.
|
|
232
|
+
* @returns the component the slot renders.
|
|
233
|
+
*/
|
|
234
|
+
function createCard(scope, t) {
|
|
235
|
+
const usePonytail = useScope(scope)
|
|
236
|
+
|
|
237
|
+
return function PonytailCard(props) {
|
|
238
|
+
const snapshot = usePonytail()
|
|
239
|
+
const [error, setError] = React.useState(null)
|
|
240
|
+
const [pending, setPending] = React.useState(false)
|
|
241
|
+
const host = useHostLevel(snapshot)
|
|
242
|
+
|
|
243
|
+
const current = shownMode(snapshot, host)
|
|
244
|
+
// A namespace this deployment does not serve renders no trace of itself.
|
|
245
|
+
if (current === undefined) return null
|
|
246
|
+
|
|
247
|
+
const user = snapshot.user !== null && typeof snapshot.user === 'object' ? snapshot.user : {}
|
|
248
|
+
const overridden = Object.prototype.hasOwnProperty.call(user, 'defaultMode')
|
|
249
|
+
const disabled = !snapshot.writable || pending
|
|
250
|
+
const selected = KNOWN_LEVELS.filter((level) => level.value === current)[0]
|
|
251
|
+
const sessionOnly = host !== null && host.source === 'session'
|
|
252
|
+
if (props != null && props.view === 'summary') {
|
|
253
|
+
return t('description', {
|
|
254
|
+
level: levelLabel(t, current),
|
|
255
|
+
note: sessionOnly ? t('fromSession') : overridden ? t('overridden') : '',
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const report = (cause) => {
|
|
260
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
261
|
+
}
|
|
262
|
+
const write = (run) => {
|
|
263
|
+
if (disabled) return
|
|
264
|
+
setError(null)
|
|
265
|
+
setPending(true)
|
|
266
|
+
new Promise((resolve) => { resolve(run()) }).then((accepted) => {
|
|
267
|
+
if (accepted === false) setError(t('refused'))
|
|
268
|
+
}).catch(report).finally(() => { setPending(false) })
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return React.createElement(
|
|
272
|
+
'div',
|
|
273
|
+
{ className: 'dp-page' },
|
|
274
|
+
React.createElement(
|
|
275
|
+
'div',
|
|
276
|
+
{ className: 'dp-body' },
|
|
277
|
+
React.createElement(
|
|
278
|
+
'div',
|
|
279
|
+
{ className: 'dp-row', role: 'radiogroup', 'aria-label': t('levelGroup') },
|
|
280
|
+
LEVELS.map((level) => React.createElement(
|
|
281
|
+
'button',
|
|
282
|
+
{
|
|
283
|
+
key: level.value,
|
|
284
|
+
type: 'button',
|
|
285
|
+
role: 'radio',
|
|
286
|
+
'aria-checked': current === level.value,
|
|
287
|
+
disabled,
|
|
288
|
+
className: `dp-pill${current === level.value ? ' dp-pill-selected' : ''}`,
|
|
289
|
+
onClick: () => { write(() => scope.set('defaultMode', level.value)) },
|
|
290
|
+
},
|
|
291
|
+
t(level.label),
|
|
292
|
+
)),
|
|
293
|
+
),
|
|
294
|
+
selected === undefined ? null : React.createElement('div', { className: 'dp-hint' }, t(selected.hint)),
|
|
295
|
+
sessionOnly ? React.createElement('div', { className: 'dp-hint' }, t('sourceSession')) : null,
|
|
296
|
+
React.createElement(
|
|
297
|
+
'div',
|
|
298
|
+
{ className: 'dp-status' },
|
|
299
|
+
snapshot.writable ? t('persists') : t('readOnly'),
|
|
300
|
+
overridden
|
|
301
|
+
? React.createElement(
|
|
302
|
+
'button',
|
|
303
|
+
{
|
|
304
|
+
type: 'button',
|
|
305
|
+
className: 'dp-reset',
|
|
306
|
+
disabled,
|
|
307
|
+
onClick: () => { write(() => scope.unset('defaultMode')) },
|
|
308
|
+
},
|
|
309
|
+
t('reset'),
|
|
310
|
+
)
|
|
311
|
+
: null,
|
|
312
|
+
),
|
|
313
|
+
error === null ? null : React.createElement('div', { className: 'dp-error' }, error),
|
|
314
|
+
),
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Build the read-only level chip for the composer tool row.
|
|
321
|
+
*
|
|
322
|
+
* It reads the same settings scope the card edits, so the two surfaces can
|
|
323
|
+
* never disagree; it renders nothing while the namespace is unavailable or
|
|
324
|
+
* the level is `off`, where a chip would only say "nothing is injected".
|
|
325
|
+
* @param scope - the scope bound to the ponytail namespace.
|
|
326
|
+
* @param t - translate function bound to this plugin's locale namespace.
|
|
327
|
+
* @returns the component the composer slot renders.
|
|
328
|
+
*/
|
|
329
|
+
function createIndicator(scope, t) {
|
|
330
|
+
const usePonytail = useScope(scope)
|
|
331
|
+
|
|
332
|
+
return function PonytailIndicator() {
|
|
333
|
+
const snapshot = usePonytail()
|
|
334
|
+
const current = shownMode(snapshot, useHostLevel(snapshot))
|
|
335
|
+
if (current === undefined || current === 'off') return null
|
|
336
|
+
|
|
337
|
+
const text = t('indicator', { level: levelLabel(t, current) })
|
|
338
|
+
return React.createElement('span', { className: 'dp-chip', title: text }, text)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Mount both browser surfaces: the Plugins card and the composer chip.
|
|
344
|
+
* @param ctx - the browser plugin context.
|
|
345
|
+
*/
|
|
346
|
+
function apply(ctx) {
|
|
347
|
+
const t = ctx.locale.bind(LOCALE_NS)
|
|
348
|
+
ctx.effect(
|
|
349
|
+
() => ctx.locale.register(LOCALE_NS, { en, zh }),
|
|
350
|
+
'dsh-ponytail: locale dictionary',
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
const scope = ctx.configForms.get(NAMESPACE)
|
|
354
|
+
const Card = createCard(scope, t)
|
|
355
|
+
const Indicator = createIndicator(scope, t)
|
|
356
|
+
|
|
357
|
+
// Each owner declares its own slot; injecting waits for it to exist, so
|
|
358
|
+
// these registrations do not depend on plugin load order.
|
|
359
|
+
ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
|
|
360
|
+
name: 'plugins.row.config',
|
|
361
|
+
key: '@maci0/dsh-ponytail#ponytail',
|
|
362
|
+
locale: LOCALE_NS,
|
|
363
|
+
}, Card))
|
|
364
|
+
|
|
365
|
+
ctx.slots.inject('conversation.input.left', () => ctx.slots.register({
|
|
366
|
+
name: 'conversation.input.left',
|
|
367
|
+
id: 'ponytail-level',
|
|
368
|
+
order: 20,
|
|
369
|
+
}, Indicator))
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
exports.apply = apply
|
|
373
|
+
exports.inject = ['slots', 'configForms', 'locale']
|
|
374
|
+
return module.exports
|
|
375
|
+
},
|
|
376
|
+
})
|