@xmoon76/dsh-pi-tui 0.2.0 → 0.2.1
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 +413 -51
- package/README.zh-CN.md +411 -0
- package/dist/builtins.mjs +1 -1
- package/dist/extensions.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-AgdnRnGA.mjs → src-C_RdKmcj.mjs} +1 -1
- package/package.json +20 -10
- package/scripts/naming-gate.mjs +33 -19
- package/scripts/tarball-smoke.mjs +3 -1
package/README.md
CHANGED
|
@@ -1,53 +1,413 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-pi-tui
|
|
2
|
+
|
|
3
|
+
English | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
Release history: [CHANGELOG.md](CHANGELOG.md) · [简体中文更新日志](CHANGELOG.zh-CN.md)
|
|
2
6
|
|
|
3
7
|
A third-party TUI mode for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`), built on a vendored fork of [pi-tui](https://github.com/MoonshotAI/kimi-code/tree/main/packages/pi-tui).
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
Run `dsh --profile pi-tui` for a terminal UI instead of the browser GUI (`dsh --profile web`) or one-shot mode (`dsh --profile headless`).
|
|
10
|
+
|
|
11
|
+
> **Status: working.** The TUI covers the main session loop — input → session events,
|
|
12
|
+
> approvals, commands, session switching and full-text search — plus presets, skills,
|
|
13
|
+
> model/settings menus, and slash commands. Rendering and input routing are verified
|
|
14
|
+
> by headless tests (`@xterm/headless`) with no TTY or model connection needed.
|
|
15
|
+
|
|
16
|
+
## Screenshot
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
## Layout
|
|
21
|
+
|
|
22
|
+
The full repository layout lives in [AGENTS.md](AGENTS.md) (the contributor
|
|
23
|
+
operating manual). In one line: the **repository root is the published
|
|
24
|
+
`@xmoon76/dsh-pi-tui` bundle** (the only published package — its manifest
|
|
25
|
+
declares `dsh.bundle.patch` and the `exports` point at the built `dist/`),
|
|
26
|
+
and `packages/pi-tui/` is the vendored `@moonshot-ai/pi-tui` fork (rescoped
|
|
27
|
+
to `@xmoon76/pi-tui`, private, never published — its divergence ledger lives
|
|
28
|
+
in `packages/pi-tui/AGENTS.md`), bundled into the root package's build
|
|
29
|
+
output.
|
|
30
|
+
|
|
31
|
+
## Prerequisites
|
|
32
|
+
|
|
33
|
+
- A DeepSeek Harness installation with profiles support (`dsh` on your `PATH`).
|
|
34
|
+
- Node >= 22.19 (`^22.19.0 || >=24`, same range as dsh). Running from source
|
|
35
|
+
needs Node with native TypeScript support (>= 23.6) or the tsx ESM hook
|
|
36
|
+
(`node --import tsx/esm`, how dsh's own source launch works).
|
|
37
|
+
- [pnpm](https://pnpm.io) only when installing from source.
|
|
6
38
|
|
|
7
39
|
## Install
|
|
8
40
|
|
|
9
|
-
|
|
41
|
+
`dsh plugin` runs pnpm inside the target profile's directory, so the usual
|
|
42
|
+
pnpm verbs (`add`, `remove`, `update`, `list`) all work.
|
|
43
|
+
|
|
44
|
+
### Option A — from the npm registry (recommended)
|
|
45
|
+
|
|
46
|
+
The published package is self-contained: the vendored pi-tui fork is bundled
|
|
47
|
+
into its build output, so `@xmoon76/dsh-pi-tui` is the only package you install
|
|
48
|
+
(`@xmoon76/pi-tui` stays private in this repo, like kimi-code keeps
|
|
49
|
+
`@moonshot-ai/pi-tui` private):
|
|
10
50
|
|
|
11
51
|
```sh
|
|
52
|
+
# install the bundle into the pi-tui profile (creates the profile if needed)
|
|
12
53
|
dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui
|
|
54
|
+
|
|
55
|
+
# run it
|
|
13
56
|
dsh --profile pi-tui
|
|
14
57
|
```
|
|
15
58
|
|
|
16
|
-
|
|
59
|
+
Any dependency whose manifest declares `dsh.bundle` joins the profile's layer
|
|
60
|
+
stack automatically — no manual `cordis.patch.yml` wiring.
|
|
17
61
|
|
|
18
|
-
|
|
62
|
+
### Option B — from source
|
|
63
|
+
|
|
64
|
+
Build artifacts are not committed (`dist/` for both packages is gitignored and
|
|
65
|
+
the package `exports` point at the built files), so build before installing
|
|
66
|
+
from a clone:
|
|
19
67
|
|
|
20
68
|
```sh
|
|
21
|
-
|
|
22
|
-
|
|
69
|
+
git clone https://github.com/XMoon/dsh-pi-tui
|
|
70
|
+
cd dsh-pi-tui
|
|
71
|
+
pnpm install
|
|
72
|
+
pnpm build # pi-tui tsdown (packages/pi-tui/dist/) + root tsdown (dist/, bundles pi-tui)
|
|
73
|
+
|
|
74
|
+
# file: — the bundle is copied into the profile at add time; rebuild + re-add
|
|
75
|
+
# to refresh (see "Update / uninstall" below)
|
|
76
|
+
dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui@file:$PWD
|
|
77
|
+
|
|
78
|
+
# link: — a live symlink instead; `pnpm build` output is picked up directly
|
|
79
|
+
dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui@link:$PWD
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Verify the install
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
dsh plugin --profile pi-tui -- list # @xmoon76/dsh-pi-tui present
|
|
86
|
+
dsh --profile pi-tui # TUI starts instead of the web GUI
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Update / uninstall
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
# registry installs:
|
|
93
|
+
dsh plugin --profile pi-tui -- update @xmoon76/dsh-pi-tui
|
|
94
|
+
# file: source installs copy at add time — rebuild + re-add to refresh
|
|
95
|
+
# (link: installs track the repo live and need only `pnpm build`):
|
|
96
|
+
pnpm build && dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui@file:$PWD
|
|
97
|
+
|
|
23
98
|
dsh plugin --profile pi-tui -- remove @xmoon76/dsh-pi-tui
|
|
24
99
|
```
|
|
25
100
|
|
|
26
|
-
##
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
pnpm install
|
|
105
|
+
pnpm build # pi-tui tsdown (packages/pi-tui/dist/) + root tsdown (dist/, bundles pi-tui)
|
|
106
|
+
pnpm test # pi-tui's own suite (node --test) + dsh-pi-tui headless tests
|
|
107
|
+
pnpm typecheck
|
|
108
|
+
node --expose-gc scripts/bench.mts # performance baseline (optional)
|
|
109
|
+
```
|
|
27
110
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- `/title [title]` — show or set the current session's title
|
|
31
|
-
- `/preset`, `/model`, `/settings`, `/export`, `/fork`, `/subagents` — see the command autocomplete (`/` + Tab)
|
|
111
|
+
Tests drive the UI through `@xterm/headless` (see `test/virtual-terminal.ts`),
|
|
112
|
+
so rendering and input routing are verified without a TTY or a model connection.
|
|
32
113
|
|
|
33
|
-
|
|
114
|
+
### Development history (dogfooding)
|
|
34
115
|
|
|
35
|
-
|
|
116
|
+
This project started development on the browser surface (`dsh --profile web`) and
|
|
117
|
+
switched to building itself with itself: since August 15 2026, all fixes and
|
|
118
|
+
features are developed inside this TUI, the same way this README and the
|
|
119
|
+
codebase are maintained. The dev loop runs on a dedicated `pi-tui-dev` profile
|
|
120
|
+
installed with Option B's `link:` specifier
|
|
121
|
+
(`dsh plugin --profile pi-tui-dev -- add @xmoon76/dsh-pi-tui@link:$PWD`)
|
|
122
|
+
— a live symlink, so `pnpm build` is picked up without re-adding — while the
|
|
123
|
+
`pi-tui` profile stays on the published registry package for real use.
|
|
36
124
|
|
|
37
|
-
|
|
125
|
+
## Extensions (early, stabilizing)
|
|
38
126
|
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
127
|
+
Since `0.2.0` the bundle ships a small, versioned extension surface so a
|
|
128
|
+
third-party Cordis plugin can contribute chrome without touching the TUI
|
|
129
|
+
internals. It is **early and stabilizing**: the capabilities below are the
|
|
130
|
+
current set; the API version (`1`) is bumped only on breaking changes, and
|
|
131
|
+
plugins must **feature-detect** capabilities instead of parsing the package
|
|
132
|
+
version.
|
|
133
|
+
|
|
134
|
+
All extension plugins remain standard DeepSeek Harness / Cordis plugins using
|
|
135
|
+
`name`, `inject`, and `apply(ctx)` against the single `piTuiExtensions`
|
|
136
|
+
service; the tiers are capability facades over that one service, not separate
|
|
137
|
+
plugin systems or runtimes.
|
|
138
|
+
|
|
139
|
+
The extension surface ships three tiers: a plugin imports ONLY the
|
|
140
|
+
public entry — never the stable entry's internals (`TuiApp`,
|
|
141
|
+
`TuiMainScreen`, `TuiAltScreen`) nor repository-relative paths.
|
|
142
|
+
|
|
143
|
+
| Tier | Entry | Contract |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| Stable | `@xmoon76/dsh-pi-tui/extensions` | compatibility-oriented; additive-first; existing semantics never silently change; removal requires a planned breaking change |
|
|
146
|
+
| Advanced | `@xmoon76/dsh-pi-tui/extensions/advanced` | experimental; minor releases may break; a migration note is required; no long-term shims |
|
|
147
|
+
| Unstable | `@xmoon76/dsh-pi-tui/extensions/unstable` | NO compatibility guarantee; implementation may change anytime |
|
|
148
|
+
|
|
149
|
+
All tiers reuse the SAME shared extension runtime: caller-fiber ownership,
|
|
150
|
+
surface lifecycle, invalidation, capability discovery. Do not fork a second
|
|
151
|
+
ownership/lifecycle model per tier. The tiers have grown since Phase 1:
|
|
152
|
+
|
|
153
|
+
- **Advanced** (`ADVANCED_API_LEVEL = 1`, Phase 2 + Phase 4): normalized
|
|
154
|
+
input capture, focused interactive surfaces (interactive managed
|
|
155
|
+
overlays), advanced editor control, the imperative UI broker
|
|
156
|
+
(select/confirm/input/notify), custom interactive UI and the host-state
|
|
157
|
+
facade (theme/title/working/tools-expanded) — still Host-mediated,
|
|
158
|
+
never raw terminal bytes. Author guide: `docs/extension-advanced.md`;
|
|
159
|
+
Pi capability reference: `docs/extension-capability-matrix.md`.
|
|
160
|
+
- **Unstable** (`UNSTABLE_API_LEVEL = 1`, Phase 3): raw input
|
|
161
|
+
interception (observe/consume/rewrite, exclusive raw ownership), the
|
|
162
|
+
Host emergency fail-safe (triple-Esc), and a selected low-level surface
|
|
163
|
+
seam — NO compatibility guarantee; a broken plugin can disrupt Host
|
|
164
|
+
behavior. Author guide: `docs/extension-unstable.md`.
|
|
165
|
+
- **Real-plugin validation (Phase 5):** the tier selection is proven by
|
|
166
|
+
real consumers in `examples/plugins/` — a
|
|
167
|
+
production-class vim modal editor (Advanced editor SDK), a
|
|
168
|
+
questionnaire form (Advanced imperative UI broker) and an interactive
|
|
169
|
+
shell (Unstable raw seam). The "which tier should I use?" decision
|
|
170
|
+
tree: `docs/plugin-authoring.md`.
|
|
171
|
+
|
|
172
|
+
A plugin imports only the public entry:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { PI_TUI_EXTENSIONS_SERVICE, type PiTuiExtensionService } from '@xmoon76/dsh-pi-tui/extensions'
|
|
176
|
+
|
|
177
|
+
export const name = 'my-plugin'
|
|
178
|
+
export const inject = ['tuiStartup', PI_TUI_EXTENSIONS_SERVICE]
|
|
179
|
+
|
|
180
|
+
export function apply(ctx: Context): void {
|
|
181
|
+
const service = ctx.get(PI_TUI_EXTENSIONS_SERVICE) as PiTuiExtensionService
|
|
182
|
+
if (!service.api().capabilities.has('slot.chrome.header.badge')) return
|
|
183
|
+
service.register<{ text: string; tone?: 'info' | 'warning' | 'error' | 'success' }>(
|
|
184
|
+
'chrome.header.badge',
|
|
185
|
+
{ id: 'my-badge', order: 100, description: 'A header badge from my plugin.' },
|
|
186
|
+
{ text: 'my-badge', tone: 'info' },
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Current extension points (v1):
|
|
192
|
+
|
|
193
|
+
| Slot | Semantics | Contribution |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| `chrome.header.badge` | list | a short `[badge]` after the host title |
|
|
196
|
+
| `input.dock.item` | list | a dock line above the todo panel |
|
|
197
|
+
| `chrome.footer.status` | list | a footer segment (host owns width/truncation) |
|
|
198
|
+
| `input.widget.above` / `input.widget.below` | list | a bounded widget around the editor (M4 component kit) |
|
|
199
|
+
|
|
200
|
+
Contributions are **plain data**, not render functions: a plugin supplies
|
|
201
|
+
`HeaderBadge` / `DockItem` / `FooterSegment` / `InputWidget` values (text +
|
|
202
|
+
semantic tone spans, or a structured `ExtensionView` tree for widgets) and
|
|
203
|
+
the host owns rendering, ANSI compilation, width budgets and truncation.
|
|
204
|
+
There is deliberately no `render(context)` callback in v1 — plugins never
|
|
205
|
+
hold a rendering context, so a contribution can never capture or mutate
|
|
206
|
+
host internals.
|
|
207
|
+
|
|
208
|
+
Since `0.2.0` the widget slots (`input.widget.above` / `input.widget.below`)
|
|
209
|
+
accept a bounded component kit: `ExtensionView` is a structured view tree
|
|
210
|
+
(`text` / `markdown` / `spacer` / `stack` / `frame` / `rows` views with
|
|
211
|
+
semantic style tokens) that the host compiles into private components. A
|
|
212
|
+
plugin can add helper rows above or below the editor — for example a status
|
|
213
|
+
widget or a quick-reference line — without touching the root layout, the
|
|
214
|
+
editor, or focus. The host owns the row budgets: under height pressure the
|
|
215
|
+
lowest-importance widgets collapse first, and the editor always survives.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
service.register<InputWidget>('input.widget.below', {
|
|
219
|
+
id: 'my-widget',
|
|
220
|
+
order: 100,
|
|
221
|
+
}, {
|
|
222
|
+
view: {
|
|
223
|
+
kind: 'text',
|
|
224
|
+
spans: [{ text: 'my-plugin ready', tone: 'success' }],
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Since M5 the extension surface also covers registries (plan §10):
|
|
230
|
+
|
|
231
|
+
- `registerCommand(contribution)` — slash-command OWNERSHIP metadata
|
|
232
|
+
(`execution: 'local' | 'submission'`): a plugin-declared local command
|
|
233
|
+
always executes directly (never steered by the busy-Enter preference);
|
|
234
|
+
a submission command flows through the session policy. Actual execution
|
|
235
|
+
stays in the host's commands service; `/name args...` keeps
|
|
236
|
+
`rawInput` verbatim. Name conflicts are reported, never guessed.
|
|
237
|
+
- `registerTheme(contribution)` — a named semantic palette selectable
|
|
238
|
+
from the /settings theme picker; the owner's unload falls back to the
|
|
239
|
+
built-in palette (a selected plugin theme never dangles).
|
|
240
|
+
- `registerSetting(contribution)` — a settings row appended to the
|
|
241
|
+
/settings panel (label + current value + choices + optional rejection);
|
|
242
|
+
the host owns the panel.
|
|
243
|
+
- `registerAutocomplete(contribution)` — an autocomplete provider
|
|
244
|
+
consulted after the host's own provider returns null (deterministic
|
|
245
|
+
order, per-provider isolation, latest-only commit).
|
|
246
|
+
- `registerKeybinding(contribution)` — normalized-key → semantic-action
|
|
247
|
+
binding, routed by the host's InputRouter (M6). A plugin declares a key
|
|
248
|
+
with the public `NormalizedKey` shape (key + ctrl/alt/shift/super) and
|
|
249
|
+
a semantic action from the host's list (`submit-draft`, `queue-draft`,
|
|
250
|
+
`steer-draft`, `cancel-activity`, `open-search`, `toggle-fullscreen`,
|
|
251
|
+
`cycle-permission`). The host normalizes ALL terminal input (Kitty
|
|
252
|
+
CSI-u, modifyOtherKeys, legacy sequences) — a plugin never sees raw
|
|
253
|
+
escape data. Reserved host lifecycle keys (Ctrl+C/D/S/F/O/T/G/J,
|
|
254
|
+
Ctrl+Enter, Enter, Esc) cannot be claimed; plain printable keys never
|
|
255
|
+
fire a binding (typing always wins); bindings are non-capturing and
|
|
256
|
+
fire LAST in the precedence ladder (after questions, approvals,
|
|
257
|
+
overlays and the editor). The action executes through the host's own
|
|
258
|
+
paths — submission/session safety is never bypassed.
|
|
259
|
+
- `registerMessageRenderer(contribution)` — a TRANSCRIPT message renderer
|
|
260
|
+
(M7, chain slot): receives a semantic `MessagePresentationSnapshot`
|
|
261
|
+
(immutable; never the mutable message or the container) and returns an
|
|
262
|
+
`ExtensionView` or `undefined` (abdicate → the next renderer → the host
|
|
263
|
+
fallback). Kind-scoped renderers apply to one message kind.
|
|
264
|
+
- `registerToolRenderer(contribution)` — a TOOL card renderer (M7, keyed
|
|
265
|
+
slot): presents the card for ONE tool name from a
|
|
266
|
+
`ToolPresentationSnapshot` (callId, toolName, status, arguments,
|
|
267
|
+
result, expanded); the winner (lowest priority) abdicates to the next
|
|
268
|
+
renderer, then the host fallback. A priority tie on the same tool name
|
|
269
|
+
is an explicit error.
|
|
270
|
+
Renderers never stall the transcript: a throwing renderer is isolated
|
|
271
|
+
and the chain continues, and the message cache embeds the renderer
|
|
272
|
+
identity + registry revision, so an HMR/unload rebuilds exactly the
|
|
273
|
+
affected components.
|
|
274
|
+
- `showOverlay(view, options)` — a MANAGED overlay lease (M8): the plugin
|
|
275
|
+
supplies an `ExtensionView` + sizing hints; the host mounts it through
|
|
276
|
+
its overlay broker (modal stacking, focus, fullscreen migration). The
|
|
277
|
+
returned lease is generation-scoped (the surface's final dispose closes
|
|
278
|
+
every still-owned lease), close() is idempotent, and hide()/show()
|
|
279
|
+
toggle visibility without closing. A plugin can never mount a raw
|
|
280
|
+
component or steal focus — the host owns the terminal and the overlay
|
|
281
|
+
stack.
|
|
282
|
+
The [extension API v1 author guide](docs/extension-api.md) records the
|
|
283
|
+
import rules, the full surface table, the lifecycle/render contracts, the
|
|
284
|
+
M11 deprecation policy and the stability contract.
|
|
285
|
+
|
|
286
|
+
The M10 acceptance fixture (plan §15): the repo ships a vim-mode fixture
|
|
287
|
+
(`test/fixtures/vim-plugin/`) that validates the editor-extension seam — the
|
|
288
|
+
packed public SDK is consumable by a third-party Cordis plugin, its
|
|
289
|
+
replacement editor receives SEMANTIC `EditorInputEvent`s (never raw terminal
|
|
290
|
+
bytes), and editor `create()`/`dispose()` work — importing ONLY
|
|
291
|
+
`@xmoon76/dsh-pi-tui/extensions`. It is NOT a production Vim and NOT a
|
|
292
|
+
Stable-API completeness proof: modal-mode behavior (insert/normal) is not
|
|
293
|
+
part of the Stable contract, and the other public surfaces (commands,
|
|
294
|
+
themes, settings, autocomplete, keybindings, renderers, overlays, widgets)
|
|
295
|
+
have their own dedicated tests. Its CI gate forbids `@xmoon76/pi-tui`,
|
|
296
|
+
`src/tui-app` and repository-relative internal paths: if a STABLE plugin
|
|
297
|
+
ever needs a private import, the SDK is missing a capability (there is no
|
|
298
|
+
`unsafeGetTuiApp()` escape hatch).
|
|
299
|
+
|
|
300
|
+
- `registerEditor(contribution)` — the EDITOR SDK (M9, plan §14):
|
|
301
|
+
single-winner by priority (a tie is an explicit error); the winner
|
|
302
|
+
occupies the editor seat through the host's ATOMIC handoff (create →
|
|
303
|
+
transfer draft/cursor → mount → focus → dispose old). A creation throw
|
|
304
|
+
keeps the current editor working; winner unload restores the next
|
|
305
|
+
winner / the host default editor WITH the draft preserved. The plugin
|
|
306
|
+
editor receives an `EditorHost` (surfaceId, generation, getSnapshot,
|
|
307
|
+
replaceText, dispatch of semantic actions submit/queue-submit/steer/
|
|
308
|
+
open-external-editor, subscribe, invalidate) — but the host still owns
|
|
309
|
+
busy-Enter, Ctrl+Enter, local-command classification, paste protection,
|
|
310
|
+
approval/question capture, session guard/lock, external editor and
|
|
311
|
+
exit: a plugin editor can never bypass those.
|
|
312
|
+
|
|
313
|
+
Lifecycle is host-owned: registrations are disposed when the plugin's Cordis
|
|
314
|
+
fiber unloads (HMR, disable), regular and fullscreen both refresh, and
|
|
315
|
+
`handle.invalidate()/replace()` re-render through the active screen. The
|
|
316
|
+
`@xmoon76/dsh-pi-tui/builtins` entry is the Loader-only first-party
|
|
317
|
+
contributor (version badge, turn/step counters, todo-summary dock item) —
|
|
318
|
+
not a stable third-party SDK. Raw terminal access, pre-host input
|
|
319
|
+
interception and full input ownership are NOT part of the Stable tier (see
|
|
320
|
+
the Advanced/Unstable roadmap).
|
|
321
|
+
|
|
322
|
+
## Slash commands (selection)
|
|
323
|
+
|
|
324
|
+
- `/sessions [query]` — open the session picker: search-as-you-type over
|
|
325
|
+
session ids, titles, and workspaces, rows grouped by workspace with live
|
|
326
|
+
`filtered/total` counts, and titles loaded in the background as they are
|
|
327
|
+
read. Enter switches to the selected session.
|
|
328
|
+
- `/search <query>` — full-text search over persisted session logs, then
|
|
329
|
+
switch to a hit.
|
|
330
|
+
- `/title [title]` / `/rename [title]` — with an argument, set the current
|
|
331
|
+
session's title (pins it against automatic generation; titles appear in
|
|
332
|
+
the `/sessions` picker); **without an argument, regenerate the title from
|
|
333
|
+
the conversation — this overwrites the current title, including one you
|
|
334
|
+
pinned earlier**.
|
|
335
|
+
- `/yolo` — switch to `danger-full-access` (alias of `/permission danger-full-access`).
|
|
336
|
+
- `/queue` — per-item queue management: edit, delete, steer one, or insert a
|
|
337
|
+
message into the agent's inbox (the queue pane above the editor shows
|
|
338
|
+
pending messages; `Ctrl+S` steers them all at once, `Alt+↑` pulls them all
|
|
339
|
+
back into the editor).
|
|
340
|
+
- `/status` — show the current session's stats and identity (turn counts,
|
|
341
|
+
token usage, workspace, installed dsh version).
|
|
342
|
+
- `/preset`, `/model`, `/settings`, `/export`, `/fork`, `/subagents` — see
|
|
343
|
+
`dsh --profile pi-tui`'s command autocomplete (`/` + Tab).
|
|
344
|
+
|
|
345
|
+
## Keybindings (selection)
|
|
346
|
+
|
|
347
|
+
- `Ctrl+F` — toggle transcript search (the `/search <query>` overlay; a
|
|
348
|
+
second press closes it).
|
|
349
|
+
- `Shift+Tab` — cycle the permission preset (read-only → workspace-write →
|
|
350
|
+
danger-full-access); the footer's mode slot badges every preset
|
|
351
|
+
(`[workspace-write]` / `[read-only]` / `[custom]`, with `[yolo]` flagging
|
|
352
|
+
the no-approval mode).
|
|
353
|
+
- `Ctrl+S` — steer: with queued messages, sends the whole queue (plus the
|
|
354
|
+
draft, if any) into the running turn at once; otherwise sends the draft
|
|
355
|
+
alone. An idle agent starts a fresh turn with everything.
|
|
356
|
+
- `Alt+↑` — dequeue: pull every queued message back into the editor draft.
|
|
357
|
+
- `Ctrl+T` — toggle the full todo list; the dock above the editor always shows
|
|
358
|
+
the todo summary and background tasks, and queued input renders between
|
|
359
|
+
them.
|
|
360
|
+
- `@` — file/folder mentions in the editor: `@` + Tab completes files from the
|
|
361
|
+
whole workspace (fd-backed when `fd` is on PATH, with a built-in recursive
|
|
362
|
+
fallback otherwise). The literal `@path` is submitted and the model reads
|
|
363
|
+
the file itself. With background work running, an empty editor's `↓` or
|
|
364
|
+
`Ctrl+J` opens the task browser over both surfaces:
|
|
365
|
+
- **subagent rows** (live continuable children) — `Enter` opens the child's
|
|
366
|
+
transcript read-only (`Esc` returns); they never register jobs records, so
|
|
367
|
+
this browser is their only glanceable home.
|
|
368
|
+
- **job rows** (bash and one-shot subagent jobs) — `Enter` shows the status
|
|
369
|
+
viewer only: a bash job's output read cursor belongs to the model's
|
|
370
|
+
`job_output`, and a one-shot subagent job record carries no child session
|
|
371
|
+
id, so the transcript is reached via `/subagents` (`s` stops a job).
|
|
372
|
+
The footer badge shows `[N tasks running · M agents · ↓ view]` while any
|
|
373
|
+
background work is live.
|
|
374
|
+
|
|
375
|
+
## Launch options
|
|
376
|
+
|
|
377
|
+
The TUI's startup row adds `--preset <id>` — the agent preset a fresh
|
|
378
|
+
session starts on (falls back to `$DSH_PI_TUI_PRESET`, then the saved
|
|
379
|
+
settings default). It exists because `/preset` only applies to a blank (not
|
|
380
|
+
yet created) session, so launch-time selection is the other half of choosing
|
|
381
|
+
a preset. All other flags are the dsh runner's own (`--session <id>`, …).
|
|
382
|
+
|
|
383
|
+
## Session lifecycle
|
|
384
|
+
|
|
385
|
+
Opening the TUI with no `--session` creates **no session at all**: the first
|
|
386
|
+
user message (text, slash command, `Ctrl+S` steer, or `!` shell) starts it
|
|
387
|
+
lazily. `--session <id>` still resumes immediately, and a local `!!` command
|
|
388
|
+
runs without needing a session.
|
|
389
|
+
|
|
390
|
+
## Verified in the P0 spike
|
|
391
|
+
|
|
392
|
+
- Vendored pi-tui: the fork's own suite passes under `node --test` (run it as
|
|
393
|
+
the sync gate after every re-vendor; the count is deliberately not copied here —
|
|
394
|
+
`packages/pi-tui/package.json` is the single source of version facts).
|
|
395
|
+
- `TuiApp` renders, accepts editor input, and handles Ctrl+C on a headless xterm.
|
|
396
|
+
- The whole import chain (pi-tui, tui-app, `@deepseek-ai/dsh-cmdline`, commander)
|
|
397
|
+
loads under the tsx ESM hook — the dsh source-launch contract.
|
|
398
|
+
- Native modifier-key addons are optional: on Linux the loader returns `undefined`
|
|
399
|
+
without attempting a load, and the non-TTY stdin path is guarded.
|
|
43
400
|
|
|
44
401
|
## Diagnostic log
|
|
45
402
|
|
|
46
|
-
The TUI writes its own diagnostics to stderr and a log file (`ctx.logger` is
|
|
403
|
+
The TUI writes its own diagnostics to stderr and a log file (`ctx.logger` is
|
|
404
|
+
invisible in this process — no exporter):
|
|
47
405
|
|
|
48
406
|
- Default file: `$DSH_HOME/logs/pi-tui-<pid>.log` (default `~/.dsh/logs/`);
|
|
49
407
|
- Line format: `[tui] <ISO time> <level> <message> k=v ...`;
|
|
50
|
-
- Default level `info`: key lifecycle events only (boot/resume/switch/exit,
|
|
408
|
+
- Default level `info`: key lifecycle events only (boot/resume/switch/exit,
|
|
409
|
+
divergence-guard warnings, errors); `debug` additionally logs every guard
|
|
410
|
+
check before a send.
|
|
51
411
|
|
|
52
412
|
Configuration (environment variables):
|
|
53
413
|
|
|
@@ -63,37 +423,39 @@ DSH_PI_TUI_LOG_LEVEL=debug dsh --profile pi-tui
|
|
|
63
423
|
tail -f ~/.dsh/logs/pi-tui-*.log
|
|
64
424
|
```
|
|
65
425
|
|
|
66
|
-
##
|
|
67
|
-
|
|
68
|
-
**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
426
|
+
## Safety & operational notes
|
|
427
|
+
|
|
428
|
+
- **One surface per session.** dsh has no cross-process session coordination:
|
|
429
|
+
a session open in TWO dsh processes (TUI + web, or two TUIs) can corrupt its
|
|
430
|
+
log. The TUI refuses to open a session already held by another live dsh
|
|
431
|
+
process (an `owner.lock` file next to the log, with a pid/starttime probe
|
|
432
|
+
for stale locks left by crashes — so the second surface is stopped at
|
|
433
|
+
OPEN time, not after the damage). For writes, the TUI detects the other
|
|
434
|
+
writer and blocks the send; the SAME action pressed again (Enter for a
|
|
435
|
+
submit, Ctrl+S for a steer, unchanged draft) forces through — an edited
|
|
436
|
+
draft, a swapped key, a new file revision, or a session switch invalidates
|
|
437
|
+
the force. Never run two surfaces on one session (full contract:
|
|
438
|
+
`docs/concurrency.md`).
|
|
439
|
+
- **Session repair.** `node_modules/@xmoon76/dsh-pi-tui/scripts/repair-session.mjs`
|
|
440
|
+
repairs corrupted logs (`--scan` lists damage read-only; `--yes` applies with
|
|
441
|
+
a mandatory backup). A torn (truncated) tail is truncated at the last
|
|
442
|
+
complete frame and reported with exact byte accounting; references to a
|
|
443
|
+
duplicated seq are never auto-resolved — the repair refuses and asks for
|
|
444
|
+
`--duplicate-reference=first|last|segment`. Repaired logs are re-verified
|
|
445
|
+
with the dsh reader's own layout checks before the backup is considered
|
|
446
|
+
redundant. (Full repair contract, incl. the frame-layout constraint:
|
|
447
|
+
`docs/repair-session.md`.)
|
|
448
|
+
- **Exit.** `/exit` (alias `/quit`) flushes the session with a 10s hard
|
|
449
|
+
timeout: a hung provider cannot trap the TUI. If the flush fails or times
|
|
450
|
+
out, the terminal prints a warning (the tail may not be persisted) and the
|
|
451
|
+
process still exits.
|
|
452
|
+
- **Performance.** `scripts/bench.mts` (non-default) measures ingest,
|
|
453
|
+
projection, cold/warm rebuilds, streaming frames, theme switches, and heap;
|
|
454
|
+
the saved baseline lives in `docs/perf-baseline.md`. Unchanged transcript
|
|
455
|
+
messages reuse their rendered components, so the warm per-frame rebuild
|
|
456
|
+
does not grow with history.
|
|
95
457
|
|
|
96
458
|
## License
|
|
97
459
|
|
|
98
|
-
MIT.
|
|
460
|
+
MIT. `packages/pi-tui` retains its upstream MIT license and authorship
|
|
99
461
|
(Copyright (c) 2025 Mario Zechner; Moonshot AI fork).
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# dsh-pi-tui
|
|
2
|
+
|
|
3
|
+
[English](README.md) | **简体中文**
|
|
4
|
+
|
|
5
|
+
发布历史:[CHANGELOG.zh-CN.md](CHANGELOG.zh-CN.md) · [English Changelog](CHANGELOG.md)
|
|
6
|
+
|
|
7
|
+
为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`) 提供的第三方 TUI 模式,构建于 [pi-tui](https://github.com/MoonshotAI/kimi-code/tree/main/packages/pi-tui) 的内置(vendored)fork 之上。
|
|
8
|
+
|
|
9
|
+
运行 `dsh --profile pi-tui` 即可获得终端界面,替代浏览器界面(`dsh --profile web`)或一次性模式(`dsh --profile headless`)。
|
|
10
|
+
|
|
11
|
+
> **状态:可用。** 该 TUI 覆盖了主会话流程——输入 → 会话事件、审批、
|
|
12
|
+
> 命令、会话切换与全文搜索——以及预设(presets)、技能(skills)、
|
|
13
|
+
> 模型/设置菜单和斜杠命令。渲染与输入路由由无头测试
|
|
14
|
+
> (`@xterm/headless`)验证,无需 TTY 或模型连接。
|
|
15
|
+
|
|
16
|
+
## 截图
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
## 目录结构
|
|
21
|
+
|
|
22
|
+
完整的仓库布局见 [AGENTS.md](AGENTS.md)(贡献者操作手册)。一句话概括:
|
|
23
|
+
**仓库根目录就是发布的 `@xmoon76/dsh-pi-tui` 包**(唯一发布的包——其
|
|
24
|
+
清单声明 `dsh.bundle.patch`,`exports` 指向构建后的 `dist/`),而
|
|
25
|
+
`packages/pi-tui/` 是 `@moonshot-ai/pi-tui` 的内置 fork(重命名为
|
|
26
|
+
`@xmoon76/pi-tui`,私有,永不发布——其分叉修改清单见
|
|
27
|
+
`packages/pi-tui/AGENTS.md`),构建时被打进根包产物。
|
|
28
|
+
|
|
29
|
+
## 前置要求
|
|
30
|
+
|
|
31
|
+
- 一个支持 profile 的 DeepSeek Harness 安装(`dsh` 已在 `PATH` 中)。
|
|
32
|
+
- Node >= 22.19(`^22.19.0 || >=24`,与 dsh 相同范围)。从源码运行需要
|
|
33
|
+
原生支持 TypeScript 的 Node(>= 23.6)或 tsx ESM 钩子
|
|
34
|
+
(`node --import tsx/esm`,dsh 自身源码启动的方式)。
|
|
35
|
+
- 仅从源码安装时需要 [pnpm](https://pnpm.io)。
|
|
36
|
+
|
|
37
|
+
## 安装
|
|
38
|
+
|
|
39
|
+
`dsh plugin` 会在目标 profile 目录内运行 pnpm,因此常用的 pnpm 动词
|
|
40
|
+
(`add`、`remove`、`update`、`list`)都可用。
|
|
41
|
+
|
|
42
|
+
### 方式 A — 从 npm registry 安装(推荐)
|
|
43
|
+
|
|
44
|
+
发布的包是自包含的:内置的 pi-tui fork 被打进了构建产物,所以只需安装
|
|
45
|
+
`@xmoon76/dsh-pi-tui` 一个包(`@xmoon76/pi-tui` 在本仓库保持私有,就像
|
|
46
|
+
kimi-code 保持 `@moonshot-ai/pi-tui` 私有一样):
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
# 把包安装到 pi-tui profile(必要时会创建该 profile)
|
|
50
|
+
dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui
|
|
51
|
+
|
|
52
|
+
# 运行
|
|
53
|
+
dsh --profile pi-tui
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
任何在清单中声明了 `dsh.bundle` 的依赖都会自动加入 profile 的层栈
|
|
57
|
+
(layer stack)——无需手动配置 `cordis.patch.yml`。
|
|
58
|
+
|
|
59
|
+
### 方式 B — 从源码安装
|
|
60
|
+
|
|
61
|
+
构建产物不提交(`dist/` 对两个包都在 gitignore 中,且包的 `exports`
|
|
62
|
+
指向构建后的文件),所以从克隆安装前需要先构建:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
git clone https://github.com/XMoon/dsh-pi-tui
|
|
66
|
+
cd dsh-pi-tui
|
|
67
|
+
pnpm install
|
|
68
|
+
pnpm build # pi-tui tsdown (packages/pi-tui/dist/) + 根 tsdown (dist/, 打入 pi-tui)
|
|
69
|
+
|
|
70
|
+
# file: — 添加时把包复制进 profile;重新构建后需重新 add 以刷新
|
|
71
|
+
#(见下方"更新 / 卸载")
|
|
72
|
+
dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui@file:$PWD
|
|
73
|
+
|
|
74
|
+
# link: — 使用实时符号链接;`pnpm build` 的产物会被直接读取
|
|
75
|
+
dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui@link:$PWD
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 验证安装
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
dsh plugin --profile pi-tui -- list # 应包含 @xmoon76/dsh-pi-tui
|
|
82
|
+
dsh --profile pi-tui # 启动的是 TUI 而不是 Web GUI
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 更新 / 卸载
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
# registry 安装:
|
|
89
|
+
dsh plugin --profile pi-tui -- update @xmoon76/dsh-pi-tui
|
|
90
|
+
# file: 源码安装在 add 时复制——重新构建并重新 add 以刷新
|
|
91
|
+
#(link: 安装实时跟踪仓库,只需 `pnpm build`):
|
|
92
|
+
pnpm build && dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui@file:$PWD
|
|
93
|
+
|
|
94
|
+
dsh plugin --profile pi-tui -- remove @xmoon76/dsh-pi-tui
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 开发
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
pnpm install
|
|
101
|
+
pnpm build # pi-tui tsdown (packages/pi-tui/dist/) + 根 tsdown (dist/, 打入 pi-tui)
|
|
102
|
+
pnpm test # pi-tui 自带套件 (node --test) + dsh-pi-tui 无头测试
|
|
103
|
+
pnpm typecheck
|
|
104
|
+
node --expose-gc scripts/bench.mts # 性能基线(可选)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
测试通过 `@xterm/headless` 驱动 UI(见
|
|
108
|
+
`test/virtual-terminal.ts`),因此无需 TTY 或模型连接
|
|
109
|
+
即可验证渲染与输入路由。
|
|
110
|
+
|
|
111
|
+
### 开发历史(dogfooding)
|
|
112
|
+
|
|
113
|
+
本项目最初在浏览器界面(`dsh --profile web`)上开发,后来转为用自己开发
|
|
114
|
+
自己:自 2026 年 8 月 15 日起,所有修复与功能都在这个 TUI 内部完成,就像
|
|
115
|
+
本 README 与代码库的维护一样。开发循环运行在专用的 `pi-tui-dev`
|
|
116
|
+
profile 上,使用方式 B 的 `link:` 说明符安装
|
|
117
|
+
(`dsh plugin --profile pi-tui-dev -- add @xmoon76/dsh-pi-tui@link:$PWD`)
|
|
118
|
+
——实时符号链接,`pnpm build` 后无需重新 add 即可生效——而 `pi-tui`
|
|
119
|
+
profile 保持安装已发布的 registry 包用于真实使用。
|
|
120
|
+
|
|
121
|
+
## 扩展(早期,稳定化中)
|
|
122
|
+
|
|
123
|
+
自 `0.2.0` 起,该 bundle 携带一个小的、带版本号的扩展面,让第三方
|
|
124
|
+
Cordis 插件无需接触 TUI 内部即可贡献 chrome。它**处于早期、稳定化中**:
|
|
125
|
+
下面的能力是当前集合;API 版本(`1`)只在破坏性变更时递增,插件必须
|
|
126
|
+
**按能力特性检测**,而不是解析包版本。
|
|
127
|
+
|
|
128
|
+
所有扩展插件仍是标准 DeepSeek Harness / Cordis 插件,使用 `name` /
|
|
129
|
+
`inject` / `apply(ctx)`,统一依赖唯一的 `piTuiExtensions` 服务;三层
|
|
130
|
+
只是该单一 Service 上的能力 facade,而非独立的插件系统或 runtime。
|
|
131
|
+
|
|
132
|
+
扩展面分为三个层级:插件只导入**公开入口**——绝不导入 Stable 入口的
|
|
133
|
+
内部(`TuiApp`、`TuiMainScreen`、`TuiAltScreen`)或仓库相对路径。
|
|
134
|
+
|
|
135
|
+
| 层级 | 入口 | 契约 |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| Stable | `@xmoon76/dsh-pi-tui/extensions` | 面向兼容;只增不改;既有语义永不静默变更;删除需计划内破坏性变更 |
|
|
138
|
+
| Advanced | `@xmoon76/dsh-pi-tui/extensions/advanced` | 实验性;minor 版本可破坏;需迁移说明;不做长期 shim |
|
|
139
|
+
| Unstable | `@xmoon76/dsh-pi-tui/extensions/unstable` | 不保证兼容;实现可随时变更 |
|
|
140
|
+
|
|
141
|
+
所有层级复用同一个共享 Extension Runtime:caller-fiber 所有权、surface
|
|
142
|
+
生命周期、失效机制、能力发现。不要按层级复制第二套所有权/生命周期
|
|
143
|
+
模型。第一阶段之后各层级已逐步落地:
|
|
144
|
+
|
|
145
|
+
- **Advanced**(`ADVANCED_API_LEVEL = 1`,Phase 2 + Phase 4):规范化输入捕获、
|
|
146
|
+
聚焦交互表面(交互式托管 overlay)、高级编辑器控制、命令式 UI broker
|
|
147
|
+
(select/confirm/input/notify)、自定义交互 UI 与 host-state facade
|
|
148
|
+
(theme/title/working/tools-expanded)——仍由 Host 中介,绝不接触 raw
|
|
149
|
+
terminal 字节。作者指南:`docs/extension-advanced.md`;Pi 能力参考:
|
|
150
|
+
`docs/extension-capability-matrix.md`。
|
|
151
|
+
- **Unstable**(`UNSTABLE_API_LEVEL = 1`,Phase 3):raw 输入拦截
|
|
152
|
+
(observe/consume/rewrite、exclusive raw 所有权)、Host 紧急 fail-safe
|
|
153
|
+
(三连 Esc)与精选低层 surface seam——不保证兼容;损坏的插件可能
|
|
154
|
+
破坏 Host 行为。作者指南:`docs/extension-unstable.md`。
|
|
155
|
+
- **真实插件验证(Phase 5):**层级选择由真实消费者验证,见
|
|
156
|
+
`examples/plugins/`——生产级 vim 模态编辑器
|
|
157
|
+
(Advanced editor SDK)、questionnaire 表单(Advanced 命令式 UI
|
|
158
|
+
broker)与交互式 shell(Unstable raw seam)。"该用哪一层?"决策树:
|
|
159
|
+
`docs/plugin-authoring.md`。
|
|
160
|
+
|
|
161
|
+
插件只导入公开入口:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { PI_TUI_EXTENSIONS_SERVICE, type PiTuiExtensionService } from '@xmoon76/dsh-pi-tui/extensions'
|
|
165
|
+
|
|
166
|
+
export const name = 'my-plugin'
|
|
167
|
+
export const inject = ['tuiStartup', PI_TUI_EXTENSIONS_SERVICE]
|
|
168
|
+
|
|
169
|
+
export function apply(ctx: Context): void {
|
|
170
|
+
const service = ctx.get(PI_TUI_EXTENSIONS_SERVICE) as PiTuiExtensionService
|
|
171
|
+
if (!service.api().capabilities.has('slot.chrome.header.badge')) return
|
|
172
|
+
service.register<{ text: string; tone?: 'info' | 'warning' | 'error' | 'success' }>(
|
|
173
|
+
'chrome.header.badge',
|
|
174
|
+
{ id: 'my-badge', order: 100, description: 'A header badge from my plugin.' },
|
|
175
|
+
{ text: 'my-badge', tone: 'info' },
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
当前扩展点(v1):
|
|
181
|
+
|
|
182
|
+
| Slot | 语义 | 贡献 |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| `chrome.header.badge` | list | host 标题后的短 `[badge]` |
|
|
185
|
+
| `input.dock.item` | list | todo 面板上方的一行 dock |
|
|
186
|
+
| `chrome.footer.status` | list | 一个 footer 段(host 拥有宽度/截断) |
|
|
187
|
+
| `input.widget.above` / `input.widget.below` | list | 编辑器周围的有界 widget(M4 组件套件) |
|
|
188
|
+
|
|
189
|
+
贡献是**纯数据**,不是 render 函数:插件提供 `HeaderBadge` / `DockItem` /
|
|
190
|
+
`FooterSegment` / `InputWidget` 值(文本 + 语义 tone spans,或 widget 的
|
|
191
|
+
结构化 `ExtensionView` 树),渲染、ANSI 编译、宽度预算与截断全部由 host
|
|
192
|
+
负责。v1 刻意没有 `render(context)` 回调——插件从不持有渲染上下文,
|
|
193
|
+
贡献永远无法捕获或修改 host 内部。
|
|
194
|
+
|
|
195
|
+
自 `0.2.0` 起,`input.widget.above` / `input.widget.below` 两个 widget slot
|
|
196
|
+
接受有界组件套件:`ExtensionView` 是结构化视图树(`text` / `markdown` /
|
|
197
|
+
`spacer` / `stack` / `frame` / `rows` 视图 + 语义样式 token),由 host 编译
|
|
198
|
+
成私有组件。插件可以在编辑器上方或下方添加辅助行——例如状态 widget
|
|
199
|
+
或快捷参考行——而不接触根布局、编辑器或焦点。行预算由 host 拥有:
|
|
200
|
+
高度不足时先折叠低 importance 的 widget,编辑器永远存活。
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
service.register<InputWidget>('input.widget.below', {
|
|
204
|
+
id: 'my-widget',
|
|
205
|
+
order: 100,
|
|
206
|
+
}, {
|
|
207
|
+
view: {
|
|
208
|
+
kind: 'text',
|
|
209
|
+
spans: [{ text: 'my-plugin ready', tone: 'success' }],
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
自 M5 起,扩展面还覆盖注册表(计划 §10):
|
|
215
|
+
|
|
216
|
+
- `registerCommand(contribution)` — 斜杠命令的**所有权**元数据
|
|
217
|
+
(`execution: 'local' | 'submission'`):插件声明的 local 命令始终直接
|
|
218
|
+
执行(永不被 busy-Enter 偏好 steer);submission 命令走会话投递策略。
|
|
219
|
+
实际执行仍在 host 的 commands 服务中;`/name args...` 保持 `rawInput`
|
|
220
|
+
原样。名称冲突被报告,绝不猜测。
|
|
221
|
+
- `registerTheme(contribution)` — 一个具名语义调色板,可在 /settings
|
|
222
|
+
主题选择器中选择;owner 卸载时回退到内置调色板(选中的插件主题
|
|
223
|
+
绝不悬空)。
|
|
224
|
+
- `registerSetting(contribution)` — 追加到 /settings 面板的设置行
|
|
225
|
+
(标签 + 当前值 + 选项 + 可选拒绝);面板由 host 拥有。
|
|
226
|
+
- `registerAutocomplete(contribution)` — 自动补全 provider,在 host 自身
|
|
227
|
+
provider 返回 null 之后按确定性顺序咨询(逐 provider 隔离、
|
|
228
|
+
latest-only commit)。
|
|
229
|
+
- `registerKeybinding(contribution)` — 归一化按键 → 语义动作的绑定,
|
|
230
|
+
由 host 的 InputRouter(M6)路由。插件用公开的 `NormalizedKey` 形状
|
|
231
|
+
(key + ctrl/alt/shift/super)声明按键和 host 动作列表中的语义动作
|
|
232
|
+
(`submit-draft`、`queue-draft`、`steer-draft`、`cancel-activity`、
|
|
233
|
+
`open-search`、`toggle-fullscreen`、`cycle-permission`)。host 统一归一化
|
|
234
|
+
所有终端输入(Kitty CSI-u、modifyOtherKeys、legacy 序列)——插件永远
|
|
235
|
+
看不到原始 escape 数据。保留的 host 生命周期按键(Ctrl+C/D/S/F/O/T/G/J、
|
|
236
|
+
Ctrl+Enter、Enter、Esc)不可被占用;纯可打印按键永不触发绑定(输入
|
|
237
|
+
永远优先);绑定是非捕获的,在优先级梯子的最后触发(question、approval、
|
|
238
|
+
overlay、编辑器之后)。动作通过 host 自己的路径执行——提交/会话安全
|
|
239
|
+
永不被绕过。
|
|
240
|
+
- `registerMessageRenderer(contribution)` — TRANSCRIPT 消息渲染器(M7,
|
|
241
|
+
chain slot):接收语义 `MessagePresentationSnapshot`(不可变;绝不是可变
|
|
242
|
+
message 或容器),返回 `ExtensionView` 或 `undefined`(弃权 → 下一个
|
|
243
|
+
渲染器 → host 回退)。按 kind 限定的渲染器只作用于对应消息类型。
|
|
244
|
+
- `registerToolRenderer(contribution)` — 工具卡片渲染器(M7,keyed slot):
|
|
245
|
+
从 `ToolPresentationSnapshot`(callId、toolName、status、arguments、
|
|
246
|
+
result、expanded)呈现一个工具名的卡片;winner(最低 priority)弃权后
|
|
247
|
+
落到下一个渲染器,再到 host 回退。同一工具名的 priority 平局是显式
|
|
248
|
+
错误。渲染器绝不会卡住 transcript:抛错的渲染器被隔离、链继续,
|
|
249
|
+
消息缓存内嵌渲染器身份 + 注册表 revision,因此 HMR/unload 只重建
|
|
250
|
+
受影响的组件。
|
|
251
|
+
- `showOverlay(view, options)` — 受管 overlay 租约(M8):插件提供
|
|
252
|
+
`ExtensionView` + 尺寸提示;host 通过其 overlay broker 挂载(模态堆叠、
|
|
253
|
+
焦点、fullscreen 迁移)。返回的租约是 generation 限定的(表面最终
|
|
254
|
+
dispose 会关闭所有仍在持有的租约),close() 幂等,hide()/show() 切换
|
|
255
|
+
可见性而不关闭。插件永远不能挂载原始组件或抢焦点——终端和 overlay
|
|
256
|
+
栈由 host 拥有。
|
|
257
|
+
[扩展 API v1 作者指南](docs/extension-api.md)记录了导入规则、完整
|
|
258
|
+
surface 表、生命周期/渲染契约、M11 弃用策略与稳定性契约。
|
|
259
|
+
|
|
260
|
+
M10 验收 fixture(计划 §15):仓库附带一个 vim-mode fixture
|
|
261
|
+
(`test/fixtures/vim-plugin/`),用于验证编辑器扩展接缝——第三方 Cordis
|
|
262
|
+
插件可消费打包后的公开 SDK,其替换编辑器通过**语义化** `EditorInputEvent`
|
|
263
|
+
接收输入(绝不接触原始终端字节),编辑器的 `create()`/`dispose()` 正常
|
|
264
|
+
工作——只导入 `@xmoon76/dsh-pi-tui/extensions`。它**不是**生产级 Vim,
|
|
265
|
+
也**不是** Stable API 完整性的证明:模态模式行为(insert/normal)不属于
|
|
266
|
+
Stable 契约,其余公开能力(命令、主题、设置、自动补全、按键绑定、渲染器、
|
|
267
|
+
overlay、widget)都有各自的独立测试。它的 CI 门禁禁止 `@xmoon76/pi-tui`、
|
|
268
|
+
`src/tui-app` 和仓库相对内部路径:如果 Stable 插件需要私有导入,说明 SDK
|
|
269
|
+
缺 capability(没有 `unsafeGetTuiApp()` 逃生舱)。
|
|
270
|
+
|
|
271
|
+
- `registerEditor(contribution)` — 编辑器 SDK(M9,计划 §14):按 priority
|
|
272
|
+
单选(平局是显式错误);winner 通过 host 的**原子交接**占据编辑器座席
|
|
273
|
+
(create → 转移 draft/cursor → mount → focus → dispose 旧编辑器)。
|
|
274
|
+
create 抛错时当前编辑器继续工作;winner 卸载恢复下一个 winner / host
|
|
275
|
+
默认编辑器,**保留 draft**。插件编辑器收到 `EditorHost`(surfaceId、
|
|
276
|
+
generation、getSnapshot、replaceText、语义动作 dispatch submit/
|
|
277
|
+
queue-submit/steer/open-external-editor、subscribe、invalidate)——但
|
|
278
|
+
host 仍然拥有 busy-Enter、Ctrl+Enter、local 命令分类、粘贴保护、
|
|
279
|
+
approval/question 捕获、会话 guard/lock、外部编辑器和退出:插件编辑器
|
|
280
|
+
永远不能绕过这些。
|
|
281
|
+
|
|
282
|
+
生命周期由 host 拥有:插件 Cordis fiber 卸载(HMR、禁用)时注册自动清理,
|
|
283
|
+
regular/fullscreen 都会刷新,`handle.invalidate()/replace()` 通过活动屏幕
|
|
284
|
+
重渲染。`@xmoon76/dsh-pi-tui/builtins` 入口是仅 Loader 使用的一方贡献者
|
|
285
|
+
(版本徽标、轮次/步骤计数器、todo 摘要 dock 项)——不是稳定的第三方
|
|
286
|
+
SDK。原始终端访问、pre-host 输入拦截与完整输入所有权**不属于 Stable
|
|
287
|
+
层级**(见 Advanced/Unstable 路线图)。
|
|
288
|
+
|
|
289
|
+
## 斜杠命令(节选)
|
|
290
|
+
|
|
291
|
+
- `/sessions [query]` — 打开会话选择器:对会话 id、标题和工作区进行
|
|
292
|
+
边输入边搜索,行按工作区分组并实时显示 `filtered/total` 计数,标题在
|
|
293
|
+
后台按需加载。回车切换到所选会话。
|
|
294
|
+
- `/search <query>` — 在持久化的会话日志中全文搜索,然后跳转到命中项。
|
|
295
|
+
- `/title [title]` / `/rename [title]` — 带参数时设置当前会话的标题
|
|
296
|
+
(固定标题,防止自动生成;标题会出现在 `/sessions` 选择器中);
|
|
297
|
+
**不带参数时根据对话重新生成标题——这会覆盖当前标题,包括你之前
|
|
298
|
+
固定的标题**。
|
|
299
|
+
- `/yolo` — 切换到 `danger-full-access`(`/permission danger-full-access`
|
|
300
|
+
的别名)。
|
|
301
|
+
- `/queue` — 逐条管理队列:编辑、删除、单条 steer,或向 agent 的 inbox
|
|
302
|
+
插入消息(编辑器上方的队列窗格显示待处理消息;`Ctrl+S` 一次性全部
|
|
303
|
+
steer,`Alt+↑` 把全部消息拉回编辑器)。
|
|
304
|
+
- `/status` — 显示当前会话的统计与身份信息(回合数、token 用量、
|
|
305
|
+
工作区、已安装的 dsh 版本)。
|
|
306
|
+
- `/preset`、`/model`、`/settings`、`/export`、`/fork`、`/subagents` —
|
|
307
|
+
见 `dsh --profile pi-tui` 的命令自动补全(`/` + Tab)。
|
|
308
|
+
|
|
309
|
+
## 快捷键(节选)
|
|
310
|
+
|
|
311
|
+
- `Ctrl+F` — 切换转录搜索(`/search <query>` 覆盖层;再按一次关闭)。
|
|
312
|
+
- `Shift+Tab` — 循环切换权限预设(read-only → workspace-write →
|
|
313
|
+
danger-full-access);页脚的模式槽位会为每个预设显示徽章
|
|
314
|
+
(`[workspace-write]` / `[read-only]` / `[custom]`,`[yolo]` 标记免审批
|
|
315
|
+
模式)。
|
|
316
|
+
- `Ctrl+S` — steer:有排队消息时,把整个队列(加上草稿,若有)一次性
|
|
317
|
+
送入正在运行的回合;否则只发送草稿。空闲的 agent 会用全部内容开启
|
|
318
|
+
新回合。
|
|
319
|
+
- `Alt+↑` — 出队:把所有排队消息拉回编辑器草稿。
|
|
320
|
+
- `Ctrl+T` — 切换完整待办列表;编辑器上方的 dock 始终显示待办摘要与
|
|
321
|
+
后台任务,排队的输入渲染在两者之间。
|
|
322
|
+
- `@` — 编辑器中的文件/文件夹提及:`@` + Tab 从整个工作区补全文件
|
|
323
|
+
(`fd` 在 PATH 上时以其为后端,否则使用内置的递归回退)。字面
|
|
324
|
+
`@path` 会被提交,由模型自行读取文件。有后台工作时,空编辑器的 `↓`
|
|
325
|
+
或 `Ctrl+J` 会打开覆盖两种表面的任务浏览器:
|
|
326
|
+
- **子代理行**(可继续的实时子任务)——`Enter` 以只读方式打开子代理
|
|
327
|
+
的转录(`Esc` 返回);它们从不注册 job 记录,所以此浏览器是它们唯一
|
|
328
|
+
可一览的归宿。
|
|
329
|
+
- **job 行**(bash 与一次性子代理 job)——`Enter` 只显示状态查看器:
|
|
330
|
+
bash job 的输出读取游标属于模型的 `job_output`,而一次性子代理 job
|
|
331
|
+
记录不带子会话 id,因此转录需通过 `/subagents` 访问(`s` 停止 job)。
|
|
332
|
+
只要有后台工作处于活动状态,页脚徽章就会显示
|
|
333
|
+
`[N tasks running · M agents · ↓ view]`。
|
|
334
|
+
|
|
335
|
+
## 启动选项
|
|
336
|
+
|
|
337
|
+
TUI 的启动行新增了 `--preset <id>`——新会话启动时使用的 agent 预设
|
|
338
|
+
(回退到 `$DSH_PI_TUI_PRESET`,然后是保存的设置默认值)。它存在的原因是
|
|
339
|
+
`/preset` 只对空白(尚未创建)会话生效,所以启动时选择是选择预设的
|
|
340
|
+
另一半。其余标志都是 dsh runner 自有的(`--session <id>`,……)。
|
|
341
|
+
|
|
342
|
+
## 会话生命周期
|
|
343
|
+
|
|
344
|
+
不带 `--session` 打开 TUI **不会创建任何会话**:第一条用户消息(文本、
|
|
345
|
+
斜杠命令、`Ctrl+S` steer 或 `!` shell)才会惰性启动会话。`--session <id>`
|
|
346
|
+
仍然立即恢复会话,本地 `!!` 命令无需会话即可运行。
|
|
347
|
+
|
|
348
|
+
## 在 P0 spike 中验证过的事项
|
|
349
|
+
|
|
350
|
+
- 内置 pi-tui:fork 自带的套件在 `node --test` 下通过(每次重新
|
|
351
|
+
vendor 后把它作为同步门禁运行;计数特意不抄在这里——
|
|
352
|
+
`packages/pi-tui/package.json` 是版本事实的唯一来源)。
|
|
353
|
+
- `TuiApp` 能在 headless xterm 上渲染、接受编辑器输入并处理 Ctrl+C。
|
|
354
|
+
- 整个导入链(pi-tui、tui-app、`@deepseek-ai/dsh-cmdline`、commander)
|
|
355
|
+
能在 tsx ESM 钩子下加载——即 dsh 源码启动契约。
|
|
356
|
+
- 原生修饰键扩展是可选的:在 Linux 上加载器返回 `undefined` 而不尝试
|
|
357
|
+
加载,非 TTY 的 stdin 路径有守卫。
|
|
358
|
+
|
|
359
|
+
## 诊断日志
|
|
360
|
+
|
|
361
|
+
TUI 把自身诊断写到 stderr 和一个日志文件(`ctx.logger` 在此进程中不可见
|
|
362
|
+
——没有 exporter):
|
|
363
|
+
|
|
364
|
+
- 默认文件:`$DSH_HOME/logs/pi-tui-<pid>.log`(默认 `~/.dsh/logs/`);
|
|
365
|
+
- 行格式:`[tui] <ISO time> <level> <message> k=v ...`;
|
|
366
|
+
- 默认级别 `info`:只记录关键生命周期事件(启动/恢复/切换/退出、
|
|
367
|
+
跨进程 guard 警告、错误);`debug` 额外记录每次发送前的 guard 检查。
|
|
368
|
+
|
|
369
|
+
配置(环境变量):
|
|
370
|
+
|
|
371
|
+
| 变量 | 含义 | 默认值 |
|
|
372
|
+
|---|---|---|
|
|
373
|
+
| `DSH_PI_TUI_LOG` | 日志文件路径;`off` 关闭文件日志 | `$DSH_HOME/logs/pi-tui-<pid>.log` |
|
|
374
|
+
| `DSH_PI_TUI_LOG_LEVEL` | `debug` / `info` / `warn` / `error` | `info` |
|
|
375
|
+
|
|
376
|
+
排障示例:
|
|
377
|
+
|
|
378
|
+
```sh
|
|
379
|
+
DSH_PI_TUI_LOG_LEVEL=debug dsh --profile pi-tui
|
|
380
|
+
tail -f ~/.dsh/logs/pi-tui-*.log
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## 安全与运维说明
|
|
384
|
+
|
|
385
|
+
- **每个会话只有一个界面。** dsh 没有跨进程的会话协调:一个会话同时
|
|
386
|
+
在两个 dsh 进程中打开(TUI + web,或两个 TUI)会损坏其日志。TUI 会
|
|
387
|
+
拒绝打开已被另一个存活 dsh 进程持有的会话(日志旁的 `owner.lock`
|
|
388
|
+
文件,带 pid/starttime 探测以处理崩溃残留的陈旧锁——第二个界面在
|
|
389
|
+
**打开时**就被阻止,而不是在损坏发生之后)。对于写入,TUI 会检测到
|
|
390
|
+
另一个写入者并阻止发送;再次按下同一个操作(Enter 提交、Ctrl+S
|
|
391
|
+
steer,草稿不变)会强制通过——编辑过的草稿、换过的按键、新的文件
|
|
392
|
+
修订或会话切换都会使强制失效。绝不要在一个会话上运行两个界面
|
|
393
|
+
(完整契约:`docs/concurrency.md`)。
|
|
394
|
+
- **会话修复。** `node_modules/@xmoon76/dsh-pi-tui/scripts/repair-session.mjs`
|
|
395
|
+
修复损坏的日志(`--scan` 只读列出损坏;`--yes` 应用修复并强制备份)。
|
|
396
|
+
撕裂(截断)的尾部会在最后一个完整帧处截断,并报告精确的字节核算;
|
|
397
|
+
指向重复 seq 的引用绝不自动解决——修复会拒绝并要求
|
|
398
|
+
`--duplicate-reference=first|last|segment`。修复后的日志在备份被认为
|
|
399
|
+
多余之前,会用 dsh 读取器自身的布局检查重新验证。(完整修复契约,
|
|
400
|
+
包括帧布局约束:`docs/repair-session.md`。)
|
|
401
|
+
- **退出。** `/exit`(`/quit` 的别名)以 10 秒硬超时刷新会话:卡死的
|
|
402
|
+
提供方无法困住 TUI。如果刷新失败或超时,终端会打印警告(尾部可能
|
|
403
|
+
未持久化),进程仍然退出。
|
|
404
|
+
- **性能。** `scripts/bench.mts`(非默认)测量摄取、投影、冷/热重建、
|
|
405
|
+
流式帧、主题切换与堆;保存的基线见 `docs/perf-baseline.md`。未变化的
|
|
406
|
+
转录消息会复用其渲染组件,因此热路径的每帧重建不会随历史增长。
|
|
407
|
+
|
|
408
|
+
## 许可证
|
|
409
|
+
|
|
410
|
+
MIT。`packages/pi-tui` 保留其上游 MIT 许可证与作者署名
|
|
411
|
+
(Copyright (c) 2025 Mario Zechner;Moonshot AI fork)。
|
package/dist/builtins.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TUI_STARTUP_SERVICE } from "./startup.mjs";
|
|
2
|
-
import { M as PI_TUI_EXTENSIONS_SERVICE } from "./src-
|
|
2
|
+
import { M as PI_TUI_EXTENSIONS_SERVICE } from "./src-C_RdKmcj.mjs";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
//#region src/builtins.ts
|
package/dist/extensions.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import "./startup.mjs";
|
|
2
|
-
import { A as inject, M as PI_TUI_EXTENSIONS_SERVICE, N as API_VERSION, P as describeKey, j as name, k as apply } from "./src-
|
|
2
|
+
import { A as inject, M as PI_TUI_EXTENSIONS_SERVICE, N as API_VERSION, P as describeKey, j as name, k as apply } from "./src-C_RdKmcj.mjs";
|
|
3
3
|
export { API_VERSION, PI_TUI_EXTENSIONS_SERVICE, apply, describeKey, inject, name };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import "./startup.mjs";
|
|
2
|
-
import { C as shouldConsumeAdvertisedMiss, D as subagentNoticeIsFailure, E as subagentJobViewHint, O as forkSeed, S as runningProfile, T as subagentJobTranscriptId, _ as name, a as SessionLockRefusedError, b as resolveInitialCatalog, c as compactingFromLog, d as foldCompactionEvent, f as foldQueueRows, g as matchPendingSubagentCall, h as isSubagentSettlementNotice, i as SESSIONLESS_COMMANDS, l as composeAgent, m as isPlainExitPrompt, n as HOST_COMMAND_CATALOG, o as apply, p as inject, r as LOCAL_COMMANDS, s as busyAfterTurnBoundary, t as Config, u as dangerCommand, v as recomposeBlank, w as shouldSteerOnEnter, x as resumeCommand, y as recordedPreset } from "./src-
|
|
2
|
+
import { C as shouldConsumeAdvertisedMiss, D as subagentNoticeIsFailure, E as subagentJobViewHint, O as forkSeed, S as runningProfile, T as subagentJobTranscriptId, _ as name, a as SessionLockRefusedError, b as resolveInitialCatalog, c as compactingFromLog, d as foldCompactionEvent, f as foldQueueRows, g as matchPendingSubagentCall, h as isSubagentSettlementNotice, i as SESSIONLESS_COMMANDS, l as composeAgent, m as isPlainExitPrompt, n as HOST_COMMAND_CATALOG, o as apply, p as inject, r as LOCAL_COMMANDS, s as busyAfterTurnBoundary, t as Config, u as dangerCommand, v as recomposeBlank, w as shouldSteerOnEnter, x as resumeCommand, y as recordedPreset } from "./src-C_RdKmcj.mjs";
|
|
3
3
|
export { Config, HOST_COMMAND_CATALOG, LOCAL_COMMANDS, SESSIONLESS_COMMANDS, SessionLockRefusedError, apply, busyAfterTurnBoundary, compactingFromLog, composeAgent, dangerCommand, foldCompactionEvent, foldQueueRows, forkSeed, inject, isPlainExitPrompt, isSubagentSettlementNotice, matchPendingSubagentCall, name, recomposeBlank, recordedPreset, resolveInitialCatalog, resumeCommand, runningProfile, shouldConsumeAdvertisedMiss, shouldSteerOnEnter, subagentJobTranscriptId, subagentJobViewHint, subagentNoticeIsFailure };
|
|
@@ -1841,7 +1841,7 @@ const markdownTheme = {
|
|
|
1841
1841
|
underline: (text) => chalk.underline(text)
|
|
1842
1842
|
};
|
|
1843
1843
|
//#endregion
|
|
1844
|
-
//#region
|
|
1844
|
+
//#region packages/pi-tui/dist/index.mjs
|
|
1845
1845
|
function fuzzyMatch(query, text) {
|
|
1846
1846
|
const queryLower = query.toLowerCase();
|
|
1847
1847
|
const textLower = text.toLowerCase();
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmoon76/dsh-pi-tui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "The dsh-pi-tui bundle: a TUI surface for DeepSeek Harness profiles (dsh --profile pi-tui)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "XMoon <xmoonox@gmail.com>",
|
|
7
7
|
"homepage": "https://github.com/XMoon/dsh-pi-tui#readme",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/XMoon/dsh-pi-tui.git"
|
|
11
|
-
"directory": "packages/dsh-pi-tui"
|
|
10
|
+
"url": "git+https://github.com/XMoon/dsh-pi-tui.git"
|
|
12
11
|
},
|
|
13
12
|
"bugs": {
|
|
14
13
|
"url": "https://github.com/XMoon/dsh-pi-tui/issues"
|
|
@@ -58,6 +57,7 @@
|
|
|
58
57
|
"scripts",
|
|
59
58
|
"cordis.patch.yml",
|
|
60
59
|
"README.md",
|
|
60
|
+
"README.zh-CN.md",
|
|
61
61
|
"LICENSE"
|
|
62
62
|
],
|
|
63
63
|
"publishConfig": {
|
|
@@ -141,23 +141,33 @@
|
|
|
141
141
|
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.8",
|
|
142
142
|
"@deepseek-ai/dsh-brand": "^0.1.0-rc.8",
|
|
143
143
|
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
|
|
144
|
-
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.8"
|
|
144
|
+
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.8",
|
|
145
|
+
"tsx": "^4.23.12"
|
|
145
146
|
},
|
|
146
147
|
"engines": {
|
|
147
|
-
"node": "
|
|
148
|
+
"node": "^22.19.0 || >=24"
|
|
148
149
|
},
|
|
149
150
|
"scripts": {
|
|
150
|
-
"build": "tsdown",
|
|
151
|
-
"build:package": "pnpm --dir ../pi-tui build && pnpm build",
|
|
152
|
-
"typecheck": "pnpm build:package && tsc -p tsconfig.json --noEmit",
|
|
153
|
-
"test": "pnpm build:package && node --test test/*.test.ts test/*.test.mjs",
|
|
154
151
|
"clean": "rm -rf dist",
|
|
152
|
+
"clean:fork": "pnpm --dir packages/pi-tui clean",
|
|
153
|
+
"build:fork": "pnpm --dir packages/pi-tui build",
|
|
154
|
+
"build:bundle": "tsdown",
|
|
155
|
+
"build": "pnpm build:fork && pnpm build:bundle",
|
|
156
|
+
"typecheck:fork": "pnpm --dir packages/pi-tui typecheck",
|
|
157
|
+
"typecheck:bundle": "tsc -p tsconfig.json --noEmit",
|
|
158
|
+
"typecheck": "pnpm typecheck:fork && pnpm typecheck:bundle",
|
|
159
|
+
"test:fork": "pnpm --dir packages/pi-tui test",
|
|
160
|
+
"test:bundle": "node --test test/*.test.ts test/*.test.mjs",
|
|
161
|
+
"test:docs": "node --test docs/tmux/*.test.mjs",
|
|
162
|
+
"test": "pnpm test:fork && pnpm test:bundle && pnpm test:docs",
|
|
155
163
|
"smoke": "node scripts/tarball-smoke.mjs",
|
|
156
164
|
"smoke:fixture": "node scripts/extension-fixture-smoke.mjs",
|
|
157
165
|
"smoke:advanced": "node scripts/advanced-plugin-smoke.mjs",
|
|
158
166
|
"smoke:phase4": "node scripts/phase4-plugin-smoke.mjs",
|
|
159
167
|
"smoke:unstable": "node scripts/unstable-plugin-smoke.mjs",
|
|
168
|
+
"smoke:vim": "node scripts/vim-plugin-smoke.mjs",
|
|
160
169
|
"smoke:examples": "node scripts/examples-plugin-smoke.mjs",
|
|
161
|
-
"
|
|
170
|
+
"pack:release": "rm -f xmoon76-dsh-pi-tui-*.tgz && pnpm pack --pack-destination .",
|
|
171
|
+
"smoke:pack": "node scripts/tarball-smoke.mjs"
|
|
162
172
|
}
|
|
163
173
|
}
|
package/scripts/naming-gate.mjs
CHANGED
|
@@ -14,7 +14,9 @@ import { spawnSync } from 'node:child_process'
|
|
|
14
14
|
import { join, dirname } from 'node:path'
|
|
15
15
|
import { fileURLToPath } from 'node:url'
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// The repository root IS the @xmoon76/dsh-pi-tui package root (root-package
|
|
18
|
+
// migration): scripts/ sits directly under it.
|
|
19
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
18
20
|
const failures = []
|
|
19
21
|
const checks = []
|
|
20
22
|
|
|
@@ -28,35 +30,34 @@ function readJson(path) {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
const rootPkg = readJson(join(ROOT, 'package.json'))
|
|
31
|
-
const bundlePkg = readJson(join(ROOT, 'packages', 'dsh-pi-tui', 'package.json'))
|
|
32
33
|
const forkPkg = readJson(join(ROOT, 'packages', 'pi-tui', 'package.json'))
|
|
33
34
|
|
|
34
35
|
// 1. Repository + profile naming: never the reserved `tui` / `dsh-tui` family.
|
|
35
|
-
check('root package name is dsh-pi-tui', rootPkg.name === 'dsh-pi-tui', rootPkg.name)
|
|
36
|
+
check('root package name is @xmoon76/dsh-pi-tui', rootPkg.name === '@xmoon76/dsh-pi-tui', rootPkg.name)
|
|
36
37
|
check('no @deepseek-ai/dsh-tui anywhere', !['@deepseek-ai/dsh-tui', 'dsh-tui'].some(name =>
|
|
37
|
-
[rootPkg,
|
|
38
|
-
for (const pkg of [rootPkg,
|
|
38
|
+
[rootPkg, forkPkg].some(pkg => pkg.name === name)))
|
|
39
|
+
for (const pkg of [rootPkg, forkPkg]) {
|
|
39
40
|
const manifest = JSON.stringify(pkg)
|
|
40
41
|
if (manifest.includes('@deepseek-ai/dsh-tui') || manifest.includes('"dsh-tui"')) {
|
|
41
42
|
check(`no dsh-tui references in ${pkg.name}`, false)
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
// The startup service keeps its stable id (internal Loader ids, fine as-is).
|
|
45
|
-
const patch = readFileSync(join(ROOT, '
|
|
46
|
+
const patch = readFileSync(join(ROOT, 'cordis.patch.yml'), 'utf8')
|
|
46
47
|
check('patch rows keep tui-startup/tui-app ids', patch.includes('id: tui-startup') && patch.includes('id: tui-app'))
|
|
47
48
|
check('startup service keeps TUI_STARTUP_SERVICE',
|
|
48
|
-
readFileSync(join(ROOT, '
|
|
49
|
+
readFileSync(join(ROOT, 'src', 'startup.ts'), 'utf8').includes("export const TUI_STARTUP_SERVICE = 'tuiStartup'"))
|
|
49
50
|
|
|
50
|
-
// 2. Package roles: the fork is private + bundled; the bundle is public.
|
|
51
|
+
// 2. Package roles: the fork is private + bundled; the root bundle is public.
|
|
51
52
|
check('fork package is private (@xmoon76/pi-tui)', forkPkg.private === true, `private=${String(forkPkg.private)}`)
|
|
52
|
-
check('bundle package is public (@xmoon76/dsh-pi-tui)',
|
|
53
|
-
check('bundle declares dsh.bundle.patch',
|
|
53
|
+
check('root bundle package is public (@xmoon76/dsh-pi-tui)', rootPkg.private !== true, `private=${String(rootPkg.private)}`)
|
|
54
|
+
check('root bundle declares dsh.bundle.patch', rootPkg.dsh?.bundle?.patch !== undefined, rootPkg.dsh?.bundle?.patch)
|
|
54
55
|
|
|
55
56
|
// 3. The fork is bundled, never a runtime dependency of the published package.
|
|
56
|
-
const tsdown = readFileSync(join(ROOT, '
|
|
57
|
+
const tsdown = readFileSync(join(ROOT, 'tsdown.config.ts'), 'utf8')
|
|
57
58
|
check('tsdown deps.onlyBundle includes @xmoon76/pi-tui',
|
|
58
59
|
tsdown.includes("onlyBundle") && tsdown.includes("'@xmoon76/pi-tui'"))
|
|
59
|
-
const runtimeDeps =
|
|
60
|
+
const runtimeDeps = rootPkg.dependencies ?? {}
|
|
60
61
|
check('@xmoon76/pi-tui is NOT a runtime dependency of the bundle',
|
|
61
62
|
!(runtimeDeps['@xmoon76/pi-tui'] !== undefined), runtimeDeps['@xmoon76/pi-tui'] ?? 'absent')
|
|
62
63
|
const forkDeps = forkPkg.dependencies ?? {}
|
|
@@ -68,14 +69,17 @@ check('fork repository.note records the vendored commit (single source of truth)
|
|
|
68
69
|
forkPkg.repository?.note ?? 'missing')
|
|
69
70
|
|
|
70
71
|
// 5. The tarball smoke + pack gate stay wired (stage G).
|
|
71
|
-
check('bundle prepack gate exists', (
|
|
72
|
-
check('bundle postpack smoke exists', (
|
|
72
|
+
check('bundle prepack gate exists', (rootPkg.scripts?.prepack ?? '').includes('pnpm build'))
|
|
73
|
+
check('bundle postpack smoke exists', (rootPkg.scripts?.postpack ?? '').includes('tarball-smoke'))
|
|
74
|
+
check('postpack keeps the pluginization smokes',
|
|
75
|
+
['extension-fixture-smoke', 'vim-plugin-smoke', 'examples-plugin-smoke'].every(name =>
|
|
76
|
+
(rootPkg.scripts?.postpack ?? '').includes(name)))
|
|
73
77
|
|
|
74
78
|
// 6. The workspace has no stray dist/ or tarballs COMMITTED (dist is
|
|
75
79
|
// gitignored by design; the gate checks git tracking, not the tree).
|
|
76
|
-
for (const
|
|
77
|
-
const tracked = spawnSync('git', ['ls-files',
|
|
78
|
-
check(
|
|
80
|
+
for (const [label, path] of [['root dist', 'dist'], ['packages/pi-tui/dist', 'packages/pi-tui/dist']]) {
|
|
81
|
+
const tracked = spawnSync('git', ['ls-files', path], { cwd: ROOT, encoding: 'utf8' }).stdout.trim()
|
|
82
|
+
check(`${label} is not committed`, tracked === '', tracked.split('\n').filter(Boolean).slice(0, 3).join(', '))
|
|
79
83
|
}
|
|
80
84
|
const tgz = spawnSync('git', ['ls-files', '*.tgz'], { cwd: ROOT, encoding: 'utf8' }).stdout.trim()
|
|
81
85
|
check('no tarballs committed', tgz === '', tgz.split('\n').filter(Boolean).slice(0, 3).join(', '))
|
|
@@ -103,7 +107,7 @@ for (const workflow of ['ci.yml', 'release.yml']) {
|
|
|
103
107
|
// crashes). `dsh-session-query` was removed by this rule: the picker
|
|
104
108
|
// types it structurally (src/sessions.ts) and reads the service off the
|
|
105
109
|
// live context, so it never needed the package.
|
|
106
|
-
const bundleSrc = join(ROOT, '
|
|
110
|
+
const bundleSrc = join(ROOT, 'src')
|
|
107
111
|
const srcFiles = (() => {
|
|
108
112
|
const out = []
|
|
109
113
|
const walk = (dir) => {
|
|
@@ -126,7 +130,7 @@ for (const file of srcFiles) {
|
|
|
126
130
|
// package that is ONLY read this way (never imported) has no place in
|
|
127
131
|
// the peer list — same rule as dsh-session-query.
|
|
128
132
|
}
|
|
129
|
-
const peers = new Set(Object.keys(
|
|
133
|
+
const peers = new Set(Object.keys(rootPkg.peerDependencies ?? {}))
|
|
130
134
|
for (const imported of [...srcImports].sort()) {
|
|
131
135
|
check(`peer declared for src import ${imported}`, peers.has(imported), peers.has(imported) ? '' : 'missing from peerDependencies')
|
|
132
136
|
}
|
|
@@ -135,6 +139,16 @@ for (const peer of [...peers].sort()) {
|
|
|
135
139
|
check(`peer ${peer} is imported by src/`, srcImports.has(peer), srcImports.has(peer) ? '' : 'no import in src/ (dead peer)')
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
// 9. The M0–M11 pluginization contract survives the root-package migration:
|
|
143
|
+
// the public SDK subpaths and their tsdown entries must all stay present.
|
|
144
|
+
const exportsMap = rootPkg.exports ?? {}
|
|
145
|
+
for (const subpath of ['./extensions', './extensions/advanced', './extensions/unstable', './builtins']) {
|
|
146
|
+
check(`exports keep ${subpath}`, exportsMap[subpath] !== undefined)
|
|
147
|
+
}
|
|
148
|
+
for (const entry of ['src/extensions.ts', 'src/builtins.ts', 'src/extension/advanced.ts', 'src/extension/unstable.ts']) {
|
|
149
|
+
check(`tsdown entry keeps ${entry}`, tsdown.includes(entry))
|
|
150
|
+
}
|
|
151
|
+
|
|
138
152
|
for (const line of checks) console.log(line)
|
|
139
153
|
if (failures.length > 0) {
|
|
140
154
|
console.error(`\nnaming-gate FAILED (${failures.length}):`)
|
|
@@ -180,7 +180,9 @@ function main() {
|
|
|
180
180
|
// the generic patterns: its source deliberately assembles the old
|
|
181
181
|
// /home/xmoon/ marker at runtime (the literal would otherwise trip the
|
|
182
182
|
// check on its own file). The dynamic-root checks still apply to it.
|
|
183
|
-
|
|
183
|
+
// After the root-package migration the repository root IS the package
|
|
184
|
+
// root, so both resolve from SCRIPT_DIR with a single `..`.
|
|
185
|
+
const repoRoot = realpathSync(join(SCRIPT_DIR, '..'))
|
|
184
186
|
const packageRoot = realpathSync(PACKAGE_ROOT)
|
|
185
187
|
// The bundle dist may be absent (a smoke run on a raw checkout); the
|
|
186
188
|
// structure checks below already fail loudly for that, so never crash
|