@sagmans/dsh-tui 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/dsh-tui-dogfood/SKILL.md +83 -0
- package/.agents/skills/dsh-tui-dogfood/references/home-state.md +78 -0
- package/.agents/skills/dsh-tui-dogfood/scripts/clone-links.mjs +133 -0
- package/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh +417 -0
- package/.agents/skills/dsh-tui-update-models/SKILL.md +121 -0
- package/.agents/skills/dsh-tui-update-models/references/model-wiring.md +162 -0
- package/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs +178 -0
- package/README.md +131 -17
- package/lib/agent/host.d.ts +11 -0
- package/lib/agent/host.d.ts.map +1 -1
- package/lib/agent/host.js +16 -2
- package/lib/agent/host.js.map +1 -1
- package/lib/agent/present.d.ts.map +1 -1
- package/lib/agent/present.js +2 -1
- package/lib/agent/present.js.map +1 -1
- package/lib/agent/undo.d.ts +52 -0
- package/lib/agent/undo.d.ts.map +1 -0
- package/lib/agent/undo.js +106 -0
- package/lib/agent/undo.js.map +1 -0
- package/lib/cards/composition.d.ts +68 -0
- package/lib/cards/composition.d.ts.map +1 -0
- package/lib/cards/composition.js +117 -0
- package/lib/cards/composition.js.map +1 -0
- package/lib/cards/presenter.d.ts +23 -0
- package/lib/cards/presenter.d.ts.map +1 -0
- package/lib/cards/presenter.js +394 -0
- package/lib/cards/presenter.js.map +1 -0
- package/lib/cards/preview.d.ts +42 -0
- package/lib/cards/preview.d.ts.map +1 -0
- package/lib/cards/preview.js +51 -0
- package/lib/cards/preview.js.map +1 -0
- package/lib/cards.d.ts +42 -95
- package/lib/cards.d.ts.map +1 -1
- package/lib/cards.js +4 -526
- package/lib/cards.js.map +1 -1
- package/lib/compat/section.d.ts +29 -0
- package/lib/compat/section.d.ts.map +1 -0
- package/lib/compat/section.js +82 -0
- package/lib/compat/section.js.map +1 -0
- package/lib/config.d.ts +27 -2
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +61 -1
- package/lib/config.js.map +1 -1
- package/lib/contracts.d.ts +28 -0
- package/lib/contracts.d.ts.map +1 -1
- package/lib/fold-cursor.d.ts +8 -0
- package/lib/fold-cursor.d.ts.map +1 -1
- package/lib/fold-cursor.js +18 -0
- package/lib/fold-cursor.js.map +1 -1
- package/lib/gates/question-card.d.ts +77 -0
- package/lib/gates/question-card.d.ts.map +1 -0
- package/lib/gates/question-card.js +113 -0
- package/lib/gates/question-card.js.map +1 -0
- package/lib/gates/questions.d.ts +127 -0
- package/lib/gates/questions.d.ts.map +1 -0
- package/lib/gates/questions.js +418 -0
- package/lib/gates/questions.js.map +1 -0
- package/lib/gates.d.ts +1 -113
- package/lib/gates.d.ts.map +1 -1
- package/lib/gates.js +1 -484
- package/lib/gates.js.map +1 -1
- package/lib/herdr/reporter.d.ts +9 -4
- package/lib/herdr/reporter.d.ts.map +1 -1
- package/lib/herdr/reporter.js +5 -9
- package/lib/herdr/reporter.js.map +1 -1
- package/lib/herdr/state.d.ts +35 -4
- package/lib/herdr/state.d.ts.map +1 -1
- package/lib/herdr/state.js +37 -4
- package/lib/herdr/state.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +343 -2267
- package/lib/index.js.map +1 -1
- package/lib/input/action-catalog.d.ts +74 -0
- package/lib/input/action-catalog.d.ts.map +1 -0
- package/lib/input/action-catalog.js +137 -0
- package/lib/input/action-catalog.js.map +1 -0
- package/lib/input/actions.d.ts +5 -81
- package/lib/input/actions.d.ts.map +1 -1
- package/lib/input/actions.js +4 -621
- package/lib/input/actions.js.map +1 -1
- package/lib/input/completion.d.ts +11 -1
- package/lib/input/completion.d.ts.map +1 -1
- package/lib/input/completion.js +17 -1
- package/lib/input/completion.js.map +1 -1
- package/lib/input/file-index.d.ts +40 -0
- package/lib/input/file-index.d.ts.map +1 -0
- package/lib/input/file-index.js +141 -0
- package/lib/input/file-index.js.map +1 -0
- package/lib/input/file-search.d.ts +1 -38
- package/lib/input/file-search.d.ts.map +1 -1
- package/lib/input/file-search.js +1 -487
- package/lib/input/file-search.js.map +1 -1
- package/lib/input/key-press.d.ts +57 -0
- package/lib/input/key-press.d.ts.map +1 -0
- package/lib/input/key-press.js +218 -0
- package/lib/input/key-press.js.map +1 -0
- package/lib/input/keymap-conflicts.d.ts +59 -0
- package/lib/input/keymap-conflicts.d.ts.map +1 -0
- package/lib/input/keymap-conflicts.js +292 -0
- package/lib/input/keymap-conflicts.js.map +1 -0
- package/lib/input/keymap-settings.js +1 -1
- package/lib/input/keymap-settings.js.map +1 -1
- package/lib/input/keymap.d.ts.map +1 -1
- package/lib/input/keymap.js +5 -1
- package/lib/input/keymap.js.map +1 -1
- package/lib/input/submission.d.ts +11 -1
- package/lib/input/submission.d.ts.map +1 -1
- package/lib/input/submission.js +10 -1
- package/lib/input/submission.js.map +1 -1
- package/lib/input/workspace-files.d.ts +10 -0
- package/lib/input/workspace-files.d.ts.map +1 -0
- package/lib/input/workspace-files.js +359 -0
- package/lib/input/workspace-files.js.map +1 -0
- package/lib/install-skills.d.ts +20 -0
- package/lib/install-skills.d.ts.map +1 -0
- package/lib/install-skills.js +198 -0
- package/lib/install-skills.js.map +1 -0
- package/lib/keys-command.d.ts +2 -1
- package/lib/keys-command.d.ts.map +1 -1
- package/lib/keys-command.js +2 -1
- package/lib/keys-command.js.map +1 -1
- package/lib/model-list.d.ts +37 -0
- package/lib/model-list.d.ts.map +1 -0
- package/lib/model-list.js +60 -0
- package/lib/model-list.js.map +1 -0
- package/lib/startup.d.ts +8 -0
- package/lib/startup.d.ts.map +1 -1
- package/lib/startup.js +120 -24
- package/lib/startup.js.map +1 -1
- package/lib/subagents.d.ts +7 -3
- package/lib/subagents.d.ts.map +1 -1
- package/lib/subagents.js +28 -4
- package/lib/subagents.js.map +1 -1
- package/lib/surface/appearance-preferences.d.ts +26 -0
- package/lib/surface/appearance-preferences.d.ts.map +1 -0
- package/lib/surface/appearance-preferences.js +139 -0
- package/lib/surface/appearance-preferences.js.map +1 -0
- package/lib/surface/appearance.d.ts +87 -0
- package/lib/surface/appearance.d.ts.map +1 -0
- package/lib/surface/appearance.js +353 -0
- package/lib/surface/appearance.js.map +1 -0
- package/lib/surface/background-work.d.ts +49 -0
- package/lib/surface/background-work.d.ts.map +1 -0
- package/lib/surface/background-work.js +146 -0
- package/lib/surface/background-work.js.map +1 -0
- package/lib/surface/commands.d.ts +80 -0
- package/lib/surface/commands.d.ts.map +1 -0
- package/lib/surface/commands.js +364 -0
- package/lib/surface/commands.js.map +1 -0
- package/lib/surface/modal-input.d.ts +58 -0
- package/lib/surface/modal-input.d.ts.map +1 -0
- package/lib/surface/modal-input.js +237 -0
- package/lib/surface/modal-input.js.map +1 -0
- package/lib/surface/model-choice.d.ts +54 -0
- package/lib/surface/model-choice.d.ts.map +1 -0
- package/lib/surface/model-choice.js +325 -0
- package/lib/surface/model-choice.js.map +1 -0
- package/lib/surface/preset-choice.d.ts +78 -0
- package/lib/surface/preset-choice.d.ts.map +1 -0
- package/lib/surface/preset-choice.js +163 -0
- package/lib/surface/preset-choice.js.map +1 -0
- package/lib/surface/prompt-input.d.ts +97 -0
- package/lib/surface/prompt-input.d.ts.map +1 -0
- package/lib/surface/prompt-input.js +215 -0
- package/lib/surface/prompt-input.js.map +1 -0
- package/lib/surface/prompt-memory.d.ts +76 -0
- package/lib/surface/prompt-memory.d.ts.map +1 -0
- package/lib/surface/prompt-memory.js +167 -0
- package/lib/surface/prompt-memory.js.map +1 -0
- package/lib/surface/session-lifecycle.d.ts +120 -0
- package/lib/surface/session-lifecycle.d.ts.map +1 -0
- package/lib/surface/session-lifecycle.js +321 -0
- package/lib/surface/session-lifecycle.js.map +1 -0
- package/lib/surface/session-picker.d.ts +39 -0
- package/lib/surface/session-picker.d.ts.map +1 -0
- package/lib/surface/session-picker.js +107 -0
- package/lib/surface/session-picker.js.map +1 -0
- package/lib/surface/session-view.d.ts +86 -0
- package/lib/surface/session-view.d.ts.map +1 -0
- package/lib/surface/session-view.js +215 -0
- package/lib/surface/session-view.js.map +1 -0
- package/lib/surface/staged-turns.d.ts +81 -0
- package/lib/surface/staged-turns.d.ts.map +1 -0
- package/lib/surface/staged-turns.js +206 -0
- package/lib/surface/staged-turns.js.map +1 -0
- package/lib/surface/terminal-lifecycle.d.ts +76 -0
- package/lib/surface/terminal-lifecycle.d.ts.map +1 -0
- package/lib/surface/terminal-lifecycle.js +222 -0
- package/lib/surface/terminal-lifecycle.js.map +1 -0
- package/lib/terminal/warning-screen.d.ts +8 -1
- package/lib/terminal/warning-screen.d.ts.map +1 -1
- package/lib/terminal/warning-screen.js +9 -0
- package/lib/terminal/warning-screen.js.map +1 -1
- package/lib/terminal-text/scan.d.ts +44 -0
- package/lib/terminal-text/scan.d.ts.map +1 -0
- package/lib/terminal-text/scan.js +195 -0
- package/lib/terminal-text/scan.js.map +1 -0
- package/lib/terminal-text/sgr.d.ts +32 -0
- package/lib/terminal-text/sgr.d.ts.map +1 -0
- package/lib/terminal-text/sgr.js +203 -0
- package/lib/terminal-text/sgr.js.map +1 -0
- package/lib/terminal-text.d.ts +1 -1
- package/lib/terminal-text.d.ts.map +1 -1
- package/lib/terminal-text.js +2 -379
- package/lib/terminal-text.js.map +1 -1
- package/lib/theme-command.js +2 -1
- package/lib/theme-command.js.map +1 -1
- package/lib/theme-defaults.d.ts +64 -0
- package/lib/theme-defaults.d.ts.map +1 -0
- package/lib/theme-defaults.js +248 -0
- package/lib/theme-defaults.js.map +1 -0
- package/lib/theme-resolver.d.ts +36 -0
- package/lib/theme-resolver.d.ts.map +1 -0
- package/lib/theme-resolver.js +112 -0
- package/lib/theme-resolver.js.map +1 -0
- package/lib/theme-schema.d.ts +6 -6
- package/lib/theme-schema.d.ts.map +1 -1
- package/lib/theme-schema.js +3 -2
- package/lib/theme-schema.js.map +1 -1
- package/lib/theme-settings.d.ts +13 -15
- package/lib/theme-settings.d.ts.map +1 -1
- package/lib/theme-settings.js +27 -19
- package/lib/theme-settings.js.map +1 -1
- package/lib/theme-tokens.d.ts +1 -90
- package/lib/theme-tokens.d.ts.map +1 -1
- package/lib/theme-tokens.js +11 -341
- package/lib/theme-tokens.js.map +1 -1
- package/lib/theme.d.ts +1 -1
- package/lib/theme.d.ts.map +1 -1
- package/lib/theme.js +2 -1
- package/lib/theme.js.map +1 -1
- package/lib/tool-display.d.ts.map +1 -1
- package/lib/tool-display.js +2 -1
- package/lib/tool-display.js.map +1 -1
- package/lib/transcript/message-content.d.ts +21 -0
- package/lib/transcript/message-content.d.ts.map +1 -0
- package/lib/transcript/message-content.js +61 -0
- package/lib/transcript/message-content.js.map +1 -0
- package/lib/transcript/tool-calls.d.ts +106 -0
- package/lib/transcript/tool-calls.d.ts.map +1 -0
- package/lib/transcript/tool-calls.js +254 -0
- package/lib/transcript/tool-calls.js.map +1 -0
- package/lib/transcript.d.ts +38 -43
- package/lib/transcript.d.ts.map +1 -1
- package/lib/transcript.js +58 -245
- package/lib/transcript.js.map +1 -1
- package/lib/ui/copy.d.ts +17 -0
- package/lib/ui/copy.d.ts.map +1 -0
- package/lib/ui/copy.js +104 -0
- package/lib/ui/copy.js.map +1 -0
- package/lib/ui/dock.d.ts +35 -3
- package/lib/ui/dock.d.ts.map +1 -1
- package/lib/ui/dock.js +124 -23
- package/lib/ui/dock.js.map +1 -1
- package/lib/ui/editor.d.ts.map +1 -1
- package/lib/ui/editor.js +2 -1
- package/lib/ui/editor.js.map +1 -1
- package/lib/ui/frame.d.ts +31 -0
- package/lib/ui/frame.d.ts.map +1 -1
- package/lib/ui/frame.js +27 -4
- package/lib/ui/frame.js.map +1 -1
- package/lib/ui/keymap-picker.d.ts +2 -1
- package/lib/ui/keymap-picker.d.ts.map +1 -1
- package/lib/ui/keymap-picker.js +1 -0
- package/lib/ui/keymap-picker.js.map +1 -1
- package/lib/ui/view/gate-card.d.ts +21 -0
- package/lib/ui/view/gate-card.d.ts.map +1 -0
- package/lib/ui/view/gate-card.js +104 -0
- package/lib/ui/view/gate-card.js.map +1 -0
- package/lib/ui/view/tool-card.d.ts +138 -0
- package/lib/ui/view/tool-card.d.ts.map +1 -0
- package/lib/ui/view/tool-card.js +413 -0
- package/lib/ui/view/tool-card.js.map +1 -0
- package/lib/ui/view/transcript-message.d.ts +62 -0
- package/lib/ui/view/transcript-message.d.ts.map +1 -0
- package/lib/ui/view/transcript-message.js +158 -0
- package/lib/ui/view/transcript-message.js.map +1 -0
- package/lib/ui/view.d.ts +87 -156
- package/lib/ui/view.d.ts.map +1 -1
- package/lib/ui/view.js +122 -556
- package/lib/ui/view.js.map +1 -1
- package/package.json +3 -1
- package/themes/deepseek-blue.yaml +9 -0
- package/themes/violet-orbit.yaml +7 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Model wiring reference
|
|
2
|
+
|
|
3
|
+
Companion to [SKILL.md](../SKILL.md): exact fields, files, and checks.
|
|
4
|
+
|
|
5
|
+
## Resolution order
|
|
6
|
+
|
|
7
|
+
1. Bundle layers in `cordis.yml`, in order.
|
|
8
|
+
2. The profile patch `~/.dsh/profiles/<profile>/cordis.patch.yml`, entry by entry:
|
|
9
|
+
`id` targeted config, `disabled`, `insert`.
|
|
10
|
+
3. A patch `config` **replaces** that row's base config; fields it omits fall back
|
|
11
|
+
to the schema default, not to the bundle's value.
|
|
12
|
+
4. The model directory a profile sees is owned by whichever row is enabled:
|
|
13
|
+
`llm-pi-ai` (additive provider config), `dsh-provider-extra` (owned catalog),
|
|
14
|
+
or both, if the profile is not using exclusive ownership.
|
|
15
|
+
|
|
16
|
+
Exclusive ownership is a set of disable rows plus the catalog itself:
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
- { "id": "agent-default-model", "disabled": true }
|
|
20
|
+
- { "id": "llm-pi-ai", "disabled": true }
|
|
21
|
+
- { "id": "llm-deepseek", "disabled": true }
|
|
22
|
+
- id: dsh-provider-extra
|
|
23
|
+
config: { catalog: { version: 1, ... } }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## `llm-pi-ai` provider config
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
- id: llm-pi-ai
|
|
30
|
+
config:
|
|
31
|
+
providers:
|
|
32
|
+
qwen-token-plan:
|
|
33
|
+
apiKeyEnv: QWEN_TOKEN_PLAN_API_KEY # env var holding the key
|
|
34
|
+
baseURL: https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1
|
|
35
|
+
api: openai-completions # optional wire override
|
|
36
|
+
models:
|
|
37
|
+
- id: qwen3.8-max # served as-is
|
|
38
|
+
- id: deepseek-v4.1-flash
|
|
39
|
+
name: DeepSeek V4.1 Flash
|
|
40
|
+
contextWindow: 1000000
|
|
41
|
+
maxTokens: 384000
|
|
42
|
+
input: [text, image]
|
|
43
|
+
reasoningEfforts: { low: low, high: high, max: max }
|
|
44
|
+
compat:
|
|
45
|
+
supportsStore: false
|
|
46
|
+
supportsDeveloperRole: false
|
|
47
|
+
supportsReasoningEffort: true
|
|
48
|
+
supportsStrictMode: true
|
|
49
|
+
thinkingFormat: qwen
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
An id that the installed catalog already describes needs no restatement. An id it
|
|
53
|
+
does not describe needs the metadata (or a `template` sibling to clone).
|
|
54
|
+
|
|
55
|
+
## `dsh-provider-extra`
|
|
56
|
+
|
|
57
|
+
Two config generations exist; a profile normally uses one:
|
|
58
|
+
|
|
59
|
+
| Config | Use |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| Additive: `routeId`, `displayName`, `apiKeyEnv`, `fallbackSessionId`, `extraModels`, `models`, `codexExtraModels`, `codexModels`, `codexTransport` | add a gateway or subscription route beside whatever else the profile serves |
|
|
62
|
+
| `catalog` v1 | own the entire directory: ids, aliases, metadata, and the default selection |
|
|
63
|
+
|
|
64
|
+
Catalog fields:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
catalog:
|
|
68
|
+
version: 1 # exactly 1
|
|
69
|
+
default: # null only when the catalog is empty
|
|
70
|
+
{ provider: opencode-go-session, model: deepseek-flash, reasoningEffort: max }
|
|
71
|
+
providers:
|
|
72
|
+
- id: opencode-go-session # the id every selection and alias check uses
|
|
73
|
+
name: OpenCode Go (session)
|
|
74
|
+
source: opencode-go # pi-ai provider id: its installed data is the base
|
|
75
|
+
baseURL: https://api.x.ai/v1 # optional
|
|
76
|
+
transport: sse # optional
|
|
77
|
+
fallbackSessionId: dsh-provider-extra # optional
|
|
78
|
+
auth: { apiKeyRef: OPENCODE_GO_API_KEY } # or { credentialProvider: openai-codex }
|
|
79
|
+
models:
|
|
80
|
+
- id: deepseek-flash
|
|
81
|
+
name: DeepSeek V4.1 Flash
|
|
82
|
+
aliases: [deepseek-v4.1-flash] # extra ids that resolve to this model
|
|
83
|
+
template: deepseek-v4-flash # clone wire behavior when the source lacks the id
|
|
84
|
+
defaultMaxTokens: 384000 # request ceiling; metadata.maxTokens overrides
|
|
85
|
+
metadata:
|
|
86
|
+
api: openai-completions
|
|
87
|
+
reasoning: true
|
|
88
|
+
input: [text, image]
|
|
89
|
+
cost: { input: 0.22, output: 0.66, cacheRead: 0.007, cacheWrite: 0 }
|
|
90
|
+
contextWindow: 1000000
|
|
91
|
+
maxTokens: 384000
|
|
92
|
+
thinkingLevelMap: { off: null, minimal: null, low: low, medium: null, high: high, xhigh: null, max: max }
|
|
93
|
+
compat: { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: true, supportsStrictMode: true, thinkingFormat: qwen }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Behaviour:
|
|
97
|
+
|
|
98
|
+
| Case | Result |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| `template` names a sibling the source ships | clone its wire behavior and metadata |
|
|
101
|
+
| `template` names nothing | that model lands in the route's diagnostics; the rest of the route still serves |
|
|
102
|
+
| a `models` whitelist names an id nothing provides | the whole route is refused - a typo there is a broken deployment |
|
|
103
|
+
| two providers share an id | composition error |
|
|
104
|
+
| `default` names a model outside the catalog | composition error |
|
|
105
|
+
| `reasoningEffort` unsupported by the model | composition error |
|
|
106
|
+
|
|
107
|
+
## Where the base data comes from
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
# the harness install owns the pi-ai catalog the profile resolves against
|
|
111
|
+
DSH=$(readlink -f "$(command -v dsh)")
|
|
112
|
+
PI_AI="${DSH%/lib/bin.js}/node_modules/@earendil-works/pi-ai"
|
|
113
|
+
ls "$PI_AI/dist/providers/data" # one JSON per provider source, keyed by api
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
A JSON entry is the whole truth for a `source`: `id`, `name`, `cost`,
|
|
117
|
+
`contextWindow`, `maxTokens`, `input`, `reasoning`, `thinkingLevelMap`, `compat`.
|
|
118
|
+
A model declared without `template` and without `metadata` is served exactly as
|
|
119
|
+
this file describes it, so an id the vendor renamed or repriced shows stale
|
|
120
|
+
numbers until the catalog declares them. An entry that carries its own complete
|
|
121
|
+
`metadata` needs no installed entry at all, which is how a route the harness does
|
|
122
|
+
not describe is wired.
|
|
123
|
+
|
|
124
|
+
## Credentials
|
|
125
|
+
|
|
126
|
+
| Field | Meaning |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `apiKeyEnv` | read the key from this environment variable |
|
|
129
|
+
| `apiKeyRef` | read it from the profiles' stored credentials under this reference |
|
|
130
|
+
| `credentialProvider` | use a login flow's stored credentials (codex subscriptions) |
|
|
131
|
+
|
|
132
|
+
Keys never belong in a patch, a settings file, or a repository. Put launch-time
|
|
133
|
+
values in `$DSH_HOME/.env` or the environment. A provider row whose credential is
|
|
134
|
+
missing fails its route only; other routes still serve.
|
|
135
|
+
|
|
136
|
+
## Verify
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
dsh --profile tui list-models # provider/model<TAB>display name, one per reachable route
|
|
140
|
+
~/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs --home "$DSH_HOME"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`list-models` exits zero only after printing a line, so a deployment that
|
|
144
|
+
advertises nothing cannot pass for one that was enumerated.
|
|
145
|
+
|
|
146
|
+
For every model you touched, confirm against the vendor's own page:
|
|
147
|
+
|
|
148
|
+
1. price per 1M tokens for input, output, cached input, cache write;
|
|
149
|
+
2. context window and maximum output tokens (a "272K" beside "above 272K input
|
|
150
|
+
is priced 2x" is a billing threshold, not the window);
|
|
151
|
+
3. the accepted reasoning effort values, and how the vendor spells "no reasoning"
|
|
152
|
+
(OpenAI: `none`; codex: omit the parameter);
|
|
153
|
+
4. whether the route bills per token at all - a credit or token plan wants cost
|
|
154
|
+
`0`, a metered API wants its published rates;
|
|
155
|
+
5. that an `xhigh` or `max` mapping exists when the vendor documents one.
|
|
156
|
+
|
|
157
|
+
A model with no vendor page (stealth releases, previews) is documented only by the
|
|
158
|
+
routing catalog: its pi.dev model page and `models.dev/api.json`.
|
|
159
|
+
|
|
160
|
+
Then run the profile against a cloned home before the real one - see the
|
|
161
|
+
`dsh-tui-dogfood` skill - and `pnpm run build` any linked bundle first: a linked
|
|
162
|
+
profile loads `lib/`, not `src/`.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Print the model directory a dsh profile resolves, with the provenance of
|
|
4
|
+
* every field.
|
|
5
|
+
*
|
|
6
|
+
* The surface reads prices, context windows, and reasoning levels from the
|
|
7
|
+
* composition, not from the vendor's page, so an inherited or stale value is
|
|
8
|
+
* invisible in the UI. This reads the same two inputs the resolver does - the
|
|
9
|
+
* owned catalog in the profile patch, and the installed pi-ai provider data -
|
|
10
|
+
* and names where each number came from: "installed" for the source's own
|
|
11
|
+
* entry, "template:<id>" for a clone, "metadata" for what the catalog declares.
|
|
12
|
+
* An entry the catalog spells out completely needs no installed entry at all,
|
|
13
|
+
* so a missing harness install must not hide it.
|
|
14
|
+
*
|
|
15
|
+
* Usage: dump-model-catalog.mjs [--home <dsh home>] [--profile <name>] [--json]
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from 'node:child_process'
|
|
18
|
+
import { readFileSync, realpathSync } from 'node:fs'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import { join } from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
|
|
23
|
+
const LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
|
24
|
+
/** Fields a catalog entry may inherit, and the layer each value came from. */
|
|
25
|
+
const INHERITED_FIELDS = ['contextWindow', 'maxTokens', 'cost', 'input', 'reasoning', 'thinkingLevelMap', 'api']
|
|
26
|
+
const INSTALLED = 'installed'
|
|
27
|
+
const METADATA = 'metadata'
|
|
28
|
+
const ABSENT = 'none'
|
|
29
|
+
|
|
30
|
+
/** The levels pi-ai offers: a null mapping removes one, and xhigh/max need one. */
|
|
31
|
+
export function supportedLevels(levelMap) {
|
|
32
|
+
return LEVELS.filter((level) => {
|
|
33
|
+
const mapped = levelMap?.[level]
|
|
34
|
+
if (mapped === null) return false
|
|
35
|
+
if (level === 'xhigh' || level === 'max') return mapped !== undefined
|
|
36
|
+
return true
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const options = { home: process.env.DSH_HOME ?? join(homedir(), '.dsh'), profile: 'tui', json: false }
|
|
42
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
43
|
+
const flag = argv[index]
|
|
44
|
+
if (flag === '--json') options.json = true
|
|
45
|
+
else if (flag === '--home') options.home = argv[++index]
|
|
46
|
+
else if (flag === '--profile') options.profile = argv[++index]
|
|
47
|
+
else throw new Error('unknown argument ' + flag)
|
|
48
|
+
}
|
|
49
|
+
return options
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The pi-ai data directory of the harness install the launcher belongs to. */
|
|
53
|
+
function dataDirectory() {
|
|
54
|
+
try {
|
|
55
|
+
const bin = execFileSync('sh', ['-c', 'command -v dsh'], { encoding: 'utf8' }).trim()
|
|
56
|
+
const root = join(realpathSync(bin), '..', '..')
|
|
57
|
+
return join(root, 'node_modules', '@earendil-works', 'pi-ai', 'dist', 'providers', 'data')
|
|
58
|
+
} catch {
|
|
59
|
+
// No harness install is a supported reading: declared entries still resolve.
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Every JSON patch entry that configures the owned catalog, last one winning. */
|
|
65
|
+
export function catalogConfig(patch) {
|
|
66
|
+
const lines = readFileSync(patch, 'utf8').split('\n')
|
|
67
|
+
let catalog
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
if (!line.startsWith('- {') || !line.includes('"catalog"')) continue
|
|
70
|
+
const entry = JSON.parse(line.slice(2))
|
|
71
|
+
if (entry.config?.catalog !== undefined) catalog = entry.config.catalog
|
|
72
|
+
}
|
|
73
|
+
return catalog
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function providerData(directory, source) {
|
|
77
|
+
if (directory === undefined) return undefined
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(readFileSync(join(directory, source + '.json'), 'utf8'))
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Look one id up across a provider's api maps; an api in the spec disambiguates. */
|
|
86
|
+
function lookup(data, id, api) {
|
|
87
|
+
if (data === undefined) return undefined
|
|
88
|
+
for (const [key, models] of Object.entries(data)) {
|
|
89
|
+
if ((api === undefined || key === api) && models[id] !== undefined) return { ...models[id], api: models[id].api ?? key }
|
|
90
|
+
}
|
|
91
|
+
return undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Resolve one catalog entry against the installed data it may inherit from. */
|
|
95
|
+
export function resolve(spec, data) {
|
|
96
|
+
const metadata = spec.metadata ?? {}
|
|
97
|
+
const declared = Object.keys(metadata).length > 0
|
|
98
|
+
const cloneOf = spec.template ?? spec.id
|
|
99
|
+
const base = lookup(data, cloneOf, metadata.api)
|
|
100
|
+
if (base === undefined && !declared) {
|
|
101
|
+
return { error: 'no source entry for ' + cloneOf + '; declare a template or metadata' }
|
|
102
|
+
}
|
|
103
|
+
const merged = { ...base, ...metadata, id: spec.id, name: spec.name ?? base?.name ?? spec.id }
|
|
104
|
+
merged.maxTokens = metadata.maxTokens ?? spec.defaultMaxTokens ?? base?.maxTokens
|
|
105
|
+
const origins = {}
|
|
106
|
+
for (const field of INHERITED_FIELDS) {
|
|
107
|
+
const declaredValue = field === 'maxTokens' ? metadata.maxTokens ?? spec.defaultMaxTokens : metadata[field]
|
|
108
|
+
if (declaredValue !== undefined) origins[field] = METADATA
|
|
109
|
+
else if (base === undefined) origins[field] = ABSENT
|
|
110
|
+
else origins[field] = spec.template === undefined ? INSTALLED : 'template:' + spec.template
|
|
111
|
+
}
|
|
112
|
+
return { model: merged, origins, levels: supportedLevels(merged.thinkingLevelMap) }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function rowsOf(catalog, directory) {
|
|
116
|
+
const rows = []
|
|
117
|
+
for (const provider of catalog.providers ?? []) {
|
|
118
|
+
const data = providerData(directory, provider.source)
|
|
119
|
+
for (const spec of provider.models ?? []) {
|
|
120
|
+
const result = resolve(spec, data)
|
|
121
|
+
const route = provider.id + '/' + spec.id
|
|
122
|
+
if (result.error !== undefined) {
|
|
123
|
+
rows.push({ route, error: result.error })
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
const cost = result.model.cost ?? {}
|
|
127
|
+
rows.push({
|
|
128
|
+
route,
|
|
129
|
+
name: result.model.name,
|
|
130
|
+
api: result.model.api,
|
|
131
|
+
contextWindow: result.model.contextWindow,
|
|
132
|
+
maxTokens: result.model.maxTokens,
|
|
133
|
+
cost: '$' + [cost.input, cost.output, cost.cacheRead, cost.cacheWrite].join('/') + ' per 1M',
|
|
134
|
+
levels: result.levels.join(','),
|
|
135
|
+
origins: INHERITED_FIELDS
|
|
136
|
+
.map(field => [field, result.origins[field]])
|
|
137
|
+
.filter(([, origin]) => origin !== INSTALLED)
|
|
138
|
+
.map(([field, origin]) => field + '=' + origin)
|
|
139
|
+
.join(' ') || 'all installed',
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return rows
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function main() {
|
|
147
|
+
const options = parseArgs(process.argv.slice(2))
|
|
148
|
+
const patch = join(options.home, 'profiles', options.profile, 'cordis.patch.yml')
|
|
149
|
+
const catalog = catalogConfig(patch)
|
|
150
|
+
if (catalog === undefined) {
|
|
151
|
+
process.stderr.write('no owned catalog in ' + patch + '\n'
|
|
152
|
+
+ 'this profile wires models another way; see references/model-wiring.md\n')
|
|
153
|
+
process.exit(1)
|
|
154
|
+
}
|
|
155
|
+
const directory = dataDirectory()
|
|
156
|
+
if (directory === undefined) {
|
|
157
|
+
process.stderr.write('no harness install found; entries that inherit from it cannot be checked\n')
|
|
158
|
+
}
|
|
159
|
+
const rows = rowsOf(catalog, directory)
|
|
160
|
+
const unresolved = rows.filter(row => row.error !== undefined).length
|
|
161
|
+
if (options.json) {
|
|
162
|
+
process.stdout.write(JSON.stringify({ default: catalog.default ?? null, rows }, null, 2) + '\n')
|
|
163
|
+
} else {
|
|
164
|
+
for (const row of rows) {
|
|
165
|
+
if (row.error !== undefined) {
|
|
166
|
+
process.stdout.write(row.route + '\n UNRESOLVED: ' + row.error + '\n')
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
process.stdout.write(row.route + '\n ' + row.name + ' | ' + row.api + ' | ctx ' + row.contextWindow
|
|
170
|
+
+ ' | max ' + row.maxTokens + ' | ' + row.cost + ' | efforts ' + row.levels + '\n inherited: ' + row.origins + '\n')
|
|
171
|
+
}
|
|
172
|
+
process.stdout.write('default: ' + JSON.stringify(catalog.default ?? null) + '\n')
|
|
173
|
+
}
|
|
174
|
+
process.exit(unresolved === 0 ? 0 : 1)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Importing the resolver must not run the CLI: the spec reads it that way.
|
|
178
|
+
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) main()
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Status: **v1 feature-complete; published on npm as `@sagmans/dsh-tui`.** The sur
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
A profile keeps this plugin as one bundle layer. Install it from a checkout of this repository, or from the registry (`0.1.0` or later). Both paths need Node.js >= 22.19 and `pnpm` on `PATH`.
|
|
9
|
+
A profile keeps this plugin as one bundle layer. Install it from a checkout of this repository, or from the registry (`0.1.0` or later). Both paths need Node.js >= 22.19 and `pnpm` on `PATH`. Interactive sessions need a real terminal: stdin and stdout must be TTYs.
|
|
10
10
|
|
|
11
11
|
### From a plugin checkout
|
|
12
12
|
|
|
@@ -30,6 +30,33 @@ dsh --profile tui
|
|
|
30
30
|
|
|
31
31
|
Releases are published, so this path works today. A checkout stays the path for unreleased work.
|
|
32
32
|
|
|
33
|
+
### Install the dogfood skill (optional)
|
|
34
|
+
|
|
35
|
+
Run this explicit command after you add the plugin:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
dsh --profile tui install-skills
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The command copies `dsh-tui-dogfood` to
|
|
42
|
+
`~/.agents/skills/dsh-tui-dogfood/`. Agents can then load it from any dsh
|
|
43
|
+
plugin repository. The skill uses a cloned dsh home, so tests do not change
|
|
44
|
+
your real profile. A first install needs no TTY. `npm install` does not copy
|
|
45
|
+
skills into your home.
|
|
46
|
+
|
|
47
|
+
If the skill exists, the command asks `Update existing skill? [y/N]` in a
|
|
48
|
+
terminal. Only `y` or `yes` replaces it. Enter or `n` keeps the existing
|
|
49
|
+
copy. To update without a prompt, including in non-interactive runs, use:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
dsh --profile tui install-skills --update
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Updating removes any local changes inside the previous skill directory.
|
|
56
|
+
The command stages the new copy before replacement and restores the old
|
|
57
|
+
copy if replacement fails. If cleanup fails after replacement, the new copy
|
|
58
|
+
stays installed and the command reports the old backup path.
|
|
59
|
+
|
|
33
60
|
### Confirm the plugin mounted
|
|
34
61
|
|
|
35
62
|
The profile records its layers in `$DSH_HOME/profiles/tui/package.json` (`~/.dsh` by default). `@sagmans/dsh-tui` must appear in `dsh.profile.bundles`:
|
|
@@ -117,8 +144,18 @@ dsh --profile tui --preset minimal # start in a shipped mode other than
|
|
|
117
144
|
dsh --profile tui --model deepseek-chat
|
|
118
145
|
dsh --profile tui --no-color
|
|
119
146
|
dsh --profile tui --no-bell # do not ring when a long turn finishes
|
|
147
|
+
dsh --profile tui list-models # print every provider/model the picker can reach
|
|
120
148
|
```
|
|
121
149
|
|
|
150
|
+
`list-models` prints one line per route the `/model` picker can reach —
|
|
151
|
+
`provider/model`, a tab, then the model's display name — in picker order:
|
|
152
|
+
providers as the llm service registered them, then each provider's own model
|
|
153
|
+
order. It writes plain stdout, so it works piped or redirected, never opens the
|
|
154
|
+
alternate screen, and exits 0 after printing at least one route. When the
|
|
155
|
+
profile has no llm service, when the provider listing cannot be read, or when
|
|
156
|
+
nothing is configured to advertise a model, it names the reason on stderr and
|
|
157
|
+
exits 1.
|
|
158
|
+
|
|
122
159
|
Every key below is a shipped default. `/keys` opens every action the surface
|
|
123
160
|
and its library can perform as a list you filter as you type, with the keys in
|
|
124
161
|
force, and the `keys:` section moves any of them — see [Keys](#keys).
|
|
@@ -140,6 +177,8 @@ force, and the `keys:` section moves any of them — see [Keys](#keys).
|
|
|
140
177
|
| Ctrl+X then Y | copy the last answer to the clipboard |
|
|
141
178
|
| Ctrl+X then E | edit the draft in `$VISUAL` (or `$EDITOR`) and take back what it saves |
|
|
142
179
|
| Ctrl+X then ? | search the key map: every action and the keys in force, in a box over the transcript |
|
|
180
|
+
| Ctrl+X then U | undo the last prompt: hide its turn, land on the previous answer, and put the prompt back in the bar |
|
|
181
|
+
| Ctrl+X then R | redo the undone prompt |
|
|
143
182
|
| `y` / `n` / Esc / Ctrl+C | allow once, reject, or cancel a pending approval |
|
|
144
183
|
| digits / space / ↑↓ / Enter / Esc / Ctrl+C | answer a question: pick or toggle, confirm, or skip one with Esc; Ctrl+C abandons the whole batch with no answers, like an aborted call; `0` answers with your own text in the input bar |
|
|
145
184
|
| ↑↓ / Ctrl+P / Ctrl+N | move through the open list: a picker's rows, a question's options, or the completion menu above the bar |
|
|
@@ -160,12 +199,15 @@ force, and the `keys:` section moves any of them — see [Keys](#keys).
|
|
|
160
199
|
| `/preset` | pick the agent mode for this session from the roster |
|
|
161
200
|
| `/preset <id>` | switch to that mode, while the session is still blank |
|
|
162
201
|
| `/new [title]` | start a fresh session without leaving the terminal (`ctrl+x` then `n` starts one untitled) |
|
|
202
|
+
| `/reload` | compose this session's agent again and replay its transcript, so an edited preset or skill file reaches the session; a running turn or a queued prompt is refused with `ctrl+c` as the way forward |
|
|
163
203
|
| `/jobs` | list background jobs with their state and duration |
|
|
164
204
|
| `/jobs read <id>` / `/jobs kill <id>` | show the tail of a job's output, or stop it |
|
|
165
205
|
| `/subagents` | list the delegations this session started, with their provider and age |
|
|
166
206
|
| `/subagents open <id\|last>` | read a child's own conversation in place; `ctrl+b` comes back |
|
|
167
207
|
| `/subagents kill <id>` | stop a live child agent |
|
|
168
208
|
| `/fork [title]` | branch this conversation after its last completed turn and continue in the branch |
|
|
209
|
+
| `/undo` | hide the newest prompt's turn and put that prompt back in the bar (`ctrl+x` then `u`) |
|
|
210
|
+
| `/redo` | step forward again after an undo (`ctrl+x` then `r`) |
|
|
169
211
|
| `/rename <title>` | title this session; the picker shows it instead of the session id |
|
|
170
212
|
| `/export [path]` | write the visible transcript as markdown (default `dsh-session-<id>.md`) |
|
|
171
213
|
| `/resume` | open another stored session without leaving the terminal |
|
|
@@ -192,18 +234,20 @@ prefix alone — enough to say that a key is waiting, without reciting the map
|
|
|
192
234
|
and a key that finishes nothing is typed as usual rather than swallowed, so a
|
|
193
235
|
prefix pressed by accident costs nothing; `/help` lists the chords, `m` for the
|
|
194
236
|
model picker, `p` for plan mode, `n` for a fresh session, `y` for the last
|
|
195
|
-
answer, `
|
|
196
|
-
reader's own editor, and `?` for
|
|
237
|
+
answer, `u` to undo the last prompt, `r` to redo it, `s` to stash the draft,
|
|
238
|
+
`l` for the stashes, `e` for the draft in the reader's own editor, and `?` for
|
|
239
|
+
the key map.
|
|
197
240
|
`keys.chord.prefix: alt+x` starts the chord with another key — or with a list of
|
|
198
241
|
them, as so many ways in — and `prefixWindow: 0` waits for the next key instead
|
|
199
242
|
of lapsing; every second key is a row of its own (`chord.model`, `chord.plan`,
|
|
200
243
|
`chord.new`, `chord.copy`, `chord.stash`, `chord.stashes`, `chord.editor`,
|
|
201
|
-
`chord.keys`), so a chord can be respelled whole. A prefix that is not a modifier
|
|
244
|
+
`chord.keys`, `chord.undo`, `chord.redo`), so a chord can be respelled whole. A prefix that is not a modifier
|
|
202
245
|
chord, that the surface or the prompt bar already answers (`ctrl+c`, `ctrl+s`),
|
|
203
246
|
or that the terminal keeps (`ctrl+q`) is refused with the reason, and the
|
|
204
247
|
shipped keymap stays in force. The chords themselves are the commands they stand
|
|
205
|
-
for: `m`, `p`, `n`, `y`, `s`, `l`, and `?` ask the same dispatcher
|
|
206
|
-
`/plan`, `/new`, `/copy`, `/stash`, `/stash-list`,
|
|
248
|
+
for: `m`, `p`, `n`, `y`, `s`, `l`, `u`, `r`, and `?` ask the same dispatcher
|
|
249
|
+
`/model`, `/plan`, `/new`, `/copy`, `/stash`, `/stash-list`, `/undo`, `/redo`,
|
|
250
|
+
and `/keys` do; `e` is the
|
|
207
251
|
one chord with no command behind it, because it opens a program rather than
|
|
208
252
|
running a line. Plan mode is the one pair that cannot share a name: `/plan` only
|
|
209
253
|
enters, so the chord names `/plan off` instead when the agent is in plan mode —
|
|
@@ -214,7 +258,9 @@ An approval or a question draws inline above the editor and takes the keyboard.
|
|
|
214
258
|
|
|
215
259
|
While a turn runs, a prompt submitted into the editor waits in the agent's own inbox instead of disappearing: it is drawn above the editor in the input bar's own frame, faint and italic, and moves into the transcript when the agent takes it — where it keeps that frame in the prompt's own mint shade, so what the reader typed is never mistaken for what the agent said. Its markdown lays out inside that frame, so a list or a fence reads in the same box it was typed into. `editor.queued` and `editor.queued.more` restyle or hide the waiting rows; `transcript.user` restyles the submitted prompt. A reply is drawn in a frame of its own, so one exchange reads as two objects rather than as a box followed by a stream of rows: `transcript.assistant.border` restyles that frame, and hiding it draws the reply bare. `ctrl+c` takes them back: an interrupt drops whatever the agent has not started, so the waiting prompts are read first and put into the bar before the turn is stopped.
|
|
216
260
|
|
|
217
|
-
|
|
261
|
+
Copying is read back through what the surface drew rather than through the screen, so a selection is the words alone: dragging across a message takes its box with it on screen, and the surface takes its own frame back out before the text reaches the clipboard — the sides, the padding beside them, and the rules above and below. A selection that covers a whole message, two of them, or a part of one is read the same way, and a row the transcript did not draw — the editor's own bar, a picker's card — is copied exactly as it read, and a selection that was nothing but frame is handed back as the reader made it, because a copy is never emptied. The frame still comes back in a copy taken with the terminal's own selection — Shift held while dragging, or a terminal that keeps selection to itself — which is the one path no program can filter.
|
|
262
|
+
|
|
263
|
+
When prompt history is enabled, each submitted line is kept in a global history at
|
|
218
264
|
`$DSH_HOME/prompt-history.json`. Typing the start of a prompt that was sent
|
|
219
265
|
before draws the rest of the newest match after the cursor in a faint shade.
|
|
220
266
|
`Ctrl+E` takes the whole suggestion and the word-movement key takes the next
|
|
@@ -319,8 +365,8 @@ must not also be a way to lose the work.
|
|
|
319
365
|
|
|
320
366
|
Every styled element is a named token with a shipped default, and every key is
|
|
321
367
|
an action with one, so the surface can be restyled and rebound without touching
|
|
322
|
-
code.
|
|
323
|
-
|
|
368
|
+
code. Released hosts with section APIs store preferences in
|
|
369
|
+
`$DSH_HOME/settings.yaml`, under a `dsh-tui:` section:
|
|
324
370
|
|
|
325
371
|
```yaml
|
|
326
372
|
dsh-tui:
|
|
@@ -355,6 +401,61 @@ dsh-tui:
|
|
|
355
401
|
hidden: true # the element renders nothing at all
|
|
356
402
|
```
|
|
357
403
|
|
|
404
|
+
Config-backed source hosts store the same preference fields in the TUI entry's
|
|
405
|
+
`config` in the active profile patch. Use the actual entry ID, normally `tui`,
|
|
406
|
+
not the legacy `dsh-tui` namespace. For example:
|
|
407
|
+
|
|
408
|
+
```yaml
|
|
409
|
+
- id: tui
|
|
410
|
+
config:
|
|
411
|
+
theme: violet-orbit
|
|
412
|
+
history:
|
|
413
|
+
enabled: false
|
|
414
|
+
ghost: false
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
The plugin exports a Config schema for all preferences shown above, including
|
|
418
|
+
`prefix`, `prefixWindow`, and `keys`. Launch fields such as `sessionId`,
|
|
419
|
+
`model`, and `provider` remain separate from live preference edits.
|
|
420
|
+
The settings service owns persistence. TUI does not create another preference store.
|
|
421
|
+
It uses released `installSection` or `register` APIs, or Config-backed
|
|
422
|
+
`describe` and revision-checked `update` APIs. Unsupported or read-only writes
|
|
423
|
+
show a notice instead of reporting success. A rejected theme selection restores
|
|
424
|
+
the applied theme, and `/theme tokens` reports that same appearance.
|
|
425
|
+
|
|
426
|
+
On Config-backed hosts, absent `history.enabled` and `history.ghost` stay off.
|
|
427
|
+
Set each switch to `true` explicitly to enable it. This prevents recording while
|
|
428
|
+
the host's asynchronous legacy import is pending or has failed. Released section
|
|
429
|
+
hosts retain their existing defaults. If the host reports unreadable preferences,
|
|
430
|
+
history stays off. Readable `false` switches survive errors in other fields.
|
|
431
|
+
Malformed updates retain the last valid appearance and do not overwrite their source.
|
|
432
|
+
|
|
433
|
+
When public descriptors expose raw user layers, history checks those layers at use
|
|
434
|
+
time. A readable opt-out survives rejected siblings even without a change event.
|
|
435
|
+
Another rejected opt-in cannot clear this protection; a valid committed update can.
|
|
436
|
+
A released-provider limit remains: after an absent user section, rejected scalar
|
|
437
|
+
sections can produce identical public descriptors. TUI cannot detect that transition
|
|
438
|
+
without host validity metadata, so normal released defaults remain active.
|
|
439
|
+
|
|
440
|
+
The inspected source host has no legacy import alias from `dsh-tui` to `tui`.
|
|
441
|
+
Exporting Config does not resolve that namespace mismatch. The host can rename
|
|
442
|
+
`settings.yaml` to `settings.yaml.imported` before import completes.
|
|
443
|
+
TUI does not retry or restore that file automatically. Preserve current privacy
|
|
444
|
+
opt-outs and copy only the intended legacy fields into the actual TUI entry's
|
|
445
|
+
`config`. Keep the original file for rollback.
|
|
446
|
+
|
|
447
|
+
Live Config edits require the loaded schema runtime to create native volatile
|
|
448
|
+
references. The released schemastery 3.18.2 implementation used by this checkout's
|
|
449
|
+
unit tests lacks that API. A source host can resolve a different implementation
|
|
450
|
+
with the same version label. TUI checks the loaded implementation and actual
|
|
451
|
+
references, not the package version or CLI identity.
|
|
452
|
+
|
|
453
|
+
If the loaded schema runtime lacks native support, TUI exports ordinary fields
|
|
454
|
+
rather than unsupported live metadata. TUI then rejects Config-backed writes,
|
|
455
|
+
and the host cannot import preferences through its volatile-field settings API.
|
|
456
|
+
Reading row preferences still works, and ordinary profile changes use the host's
|
|
457
|
+
reload lifecycle. Native schema-backed hosts can apply live preference updates.
|
|
458
|
+
|
|
358
459
|
Every field is optional, so a section that changes one shade is enough. The
|
|
359
460
|
document is hot-reloaded: an edit restyles a running session and re-arms the
|
|
360
461
|
keymap on the next press, and `/theme` shows each element's effective value and
|
|
@@ -609,11 +710,11 @@ A question whose id ends in `:secret` declares its typed answer a credential: th
|
|
|
609
710
|
|
|
610
711
|
The fold is durable-only: the live stream decorates the row that is still being written, and everything else — cards, reasoning, work state, compaction markers — comes from the log, so a resumed session renders what the live one did. Subagent start and finish are the exception: they arrive as service events, and the transcript shows them as decoration because the durable record of a delegation is the tool call that asked for it.
|
|
611
712
|
|
|
612
|
-
Tool cards are folded by default: a card draws one header row — the tool, its argument clipped to the configured budget, and the facts the result measured — so a long read, diff, or search cannot bury the conversation. A shell card's row also carries the exit status and the count of output rows waiting behind the fold, because its output is the answer the reader asked for and a fold that left no trace of it would read as a call that produced nothing. Clicking a card opens or folds that one message; `Ctrl+O` opens or folds every card at once, and `tools:` in the [settings](#settings) decides how each tool starts and whether a fold hides its rows or keeps a `tail` of them.
|
|
713
|
+
Tool cards are folded by default: a card draws one header row — the tool, its argument clipped to the configured budget, and the facts the result measured — so a long read, diff, or search cannot bury the conversation. A call that has not answered yet names itself in the running colour, `tool.running.title`, and ends that row with the whole seconds it has been waiting, in `tool.running.elapsed`, once it has waited one, so a reader can tell the call they are watching from the one below it that already finished; both give way to the measured facts the moment the result lands. A call that failed names itself in the failed colour, `tool.failed.title` — including a shell whose command exited non-zero or died on a signal, which is a failure whether or not the tool that ran it said so. A shell card's row also carries the exit status and the count of output rows waiting behind the fold, because its output is the answer the reader asked for and a fold that left no trace of it would read as a call that produced nothing. Clicking a card opens or folds that one message; `Ctrl+O` opens or folds every card at once, and `tools:` in the [settings](#settings) decides how each tool starts and whether a fold hides its rows or keeps a `tail` of them.
|
|
613
714
|
|
|
614
|
-
A PTC card is the one card with children: every call the `run_code` program dispatched hangs off the card that made it, and each draws under the header as the tool's own name and argument, on one row cut at the screen edge whether the card itself is open or folded — a program's work must stay legible without opening its card. Clicking one of those rows opens that call's argument in full and leaves its neighbours and the card as they were; a shell call also brings back the rows it printed, because the program's return value is all the card itself keeps. `Ctrl+Y` hides or shows them all, and `subcalls: collapsed` starts every session with them hidden; see [Settings](#settings).
|
|
715
|
+
A PTC card is the one card with children: every call the `run_code` program dispatched hangs off the card that made it, and each draws under the header as the tool's own name and argument, on one row cut at the screen edge whether the card itself is open or folded — a program's work must stay legible without opening its card. Each of those rows names its own call in the colour of what that call is doing — `tool.subcall.running` while the program is waiting on it, `tool.failed.title` when it failed, and `tool.subcall.title` once it is back — and a settled row keeps the one line the tool itself drew about its outcome, in `tool.terminal.status` where that tool declares one. A shell call therefore reads `bash pnpm test · exit 0` when it worked and the same row in red, with `exit 1`, when it did not, so a program's work reads row by row without opening the card. The program's own row is the one card that shows no mark: its timer is what says it is still running, and it is also what the card keeps when the program answers — the total it ran for, drawn in `tool.elapsed.done`, dimmed and italic, because the work it measured is over. Clicking one of those rows opens that call's argument in full and leaves its neighbours and the card as they were; a shell call also brings back the rows it printed, because the program's return value is all the card itself keeps. `Ctrl+Y` hides or shows them all, and `subcalls: collapsed` starts every session with them hidden; see [Settings](#settings).
|
|
615
716
|
|
|
616
|
-
A card's header names the tool, then the argument the call was made with — a path or a command — in the `tool.args` colour, then the facts the result measured: a read reports its line range, line count, and token size; a file change that carried no prior content to compare against reports its lines and tokens; one that did reports added, changed, and removed lines as `+n ~n -n` in green, yellow, and red. Each stat is its own token, so any of them can be recoloured or hidden independently.
|
|
717
|
+
A card's header names the tool, then the argument the call was made with — a path or a command — in the `tool.args` colour, then the facts the result measured: a read reports its line range, line count, and token size; a file change that carried no prior content to compare against reports its lines and tokens; one that did reports added, changed, and removed lines as `+n ~n -n` in green, yellow, and red. Each stat is its own token, so any of them can be recoloured or hidden independently, and so are the elements a call in flight is drawn with — hiding `tool.running.elapsed` leaves the name in its running colour, hiding `tool.running.title` leaves the seconds counting, hiding `tool.elapsed.done` takes the total off a program's settled row, and a dispatched row is toned the same way per state with `tool.subcall.running` and `tool.subcall.title`. A state is only how a name is painted, so hiding one of those colours leaves the name in the colour a call with no state is read in rather than taking the name away.
|
|
617
718
|
|
|
618
719
|
The bundle also takes the base's global agent rows out of the composition, twenty-three of them. Every one is a row the shipped modes supply per session instead, so leaving it mounted registers the same tool names in two layers and doubles each prompt section it owns. What stays mounted is the host: sessions, storage, models, permissions, jobs, and the command registry.
|
|
619
720
|
|
|
@@ -649,14 +750,14 @@ The list the dock and `/todo` draw follows the same lifetime every other surface
|
|
|
649
750
|
| This surface | Herdr |
|
|
650
751
|
|---|---|
|
|
651
752
|
| the screen is taken, before any session opens | `idle`, claiming the pane's agent row |
|
|
652
|
-
|
|
|
753
|
+
| the agent's driver starts, including a run of turns chained through pending work | `working` |
|
|
653
754
|
| an approval, a question, or a picker takes the keyboard | `blocked`, with that card's title sent along |
|
|
654
|
-
| the decision settles | `working` if
|
|
655
|
-
|
|
|
755
|
+
| the decision settles | `working` if the driver is running, otherwise `idle` |
|
|
756
|
+
| the agent's driver stops with nothing pending | `idle` |
|
|
656
757
|
| a session opens, resumes, forks, or is switched to | its id and reason, plus the `dsh_session` / `dsh_cwd` pane tokens |
|
|
657
758
|
| exit, signal, or boot failure | `herdr pane release-agent`, so no row is left waiting on a process that is gone |
|
|
658
759
|
|
|
659
|
-
A wait outranks a running
|
|
760
|
+
A wait outranks a running driver: an agent waiting on a human is not making progress, and the wait is the only thing worth acting on from a wall of panes. The pane reports the agent's driver rather than each turn: the harness chains turns through a pending inbox inside one driver run, and reporting each turn's end would read as done between two turns of an agent that is still working. Reports are sequenced per source, so a delivery that arrives late cannot undo the state the surface already moved past, and a state Herdr is already showing is not sent again. A report is only counted as made when Herdr acknowledges it: one that failed is tried again — soon after, then with a growing wait — and a session identity Herdr never confirmed travels with the next report of any kind. The retry cannot wait for another state change, because the pane may have nothing left to say: a driver that stopped while the socket was down produces no further event. States still waiting to be sent are collapsed into the newest one, so a socket that was down for a minute is told where the pane is rather than where it has been. The release carries the next number in that same sequence for the same reason: Herdr reads one that cannot beat the pane's last report as stale, and a stale release leaves the row waiting on a process that is gone. It also stops reporting first — a claim landing after the release would take the row back for a process that is leaving — and the report already on the wire is waited for before the row goes back, because Herdr ignores the release of a pane nothing has claimed yet and that late report would then claim it. What is still waiting behind it is dropped rather than sent.
|
|
660
761
|
|
|
661
762
|
Herdr persists a session reference only for its own built-in integrations, so this pane's session identity travels as metadata tokens instead: a script or a companion plugin reads them back with `herdr pane get <id>` and resumes that exact conversation with `dsh --profile tui --resume=<id>`. Herdr holds a token value up to 80 characters and shortens anything longer, so a session id or directory that cannot be sent whole has its token cleared instead: a shortened path would read as a different directory, and a pane must not claim one. What Herdr cannot do is identify the process itself — its detection table and its screen rules both name built-in agents only — so a pane that has not reported yet reads as an ordinary pane, and is not yet a target: `herdr agent wait` on it fails with `agent_not_found` until the first report lands. Herdr 0.9.1 also keeps the title that accompanies a `blocked` state without showing it anywhere, so a reader sees the state and reads the card on screen.
|
|
662
763
|
|
|
@@ -688,6 +789,9 @@ node tools/pty-drive.mjs --home "$S" --prompt 'Reply with exactly: pong'
|
|
|
688
789
|
|
|
689
790
|
Test specs import plugin sources through the `@/` alias. Under this test runner the spec file is resolved with a root-relative id, so parent-relative imports (`../src/...`) do not resolve; the alias and its matching `tsconfig.test.json` path mapping avoid that.
|
|
690
791
|
|
|
792
|
+
The development guide — the gate, the dogfood script, and the difference between
|
|
793
|
+
a fresh home, a cloned home, and the real one — is [DEVELOPMENT.md](DEVELOPMENT.md).
|
|
794
|
+
|
|
691
795
|
## Manual acceptance
|
|
692
796
|
|
|
693
797
|
The automated checks drive a real PTY, but they run on this machine's terminal. These are the checks only a terminal on your desk can answer; each line is what to do and what it should look like.
|
|
@@ -708,6 +812,12 @@ The automated checks drive a real PTY, but they run on this machine's terminal.
|
|
|
708
812
|
| `dsh-tui: { subcalls: collapsed }` in `$DSH_HOME/settings.yaml`, then a PTC turn | the card arrives alone, and editing the document to `inline` draws the one-line calls in a running session |
|
|
709
813
|
| a thought, folded | a click on the row opens the thought under its summary; a click on the body folds it back, and the other thoughts keep their own state |
|
|
710
814
|
| a bash card, then a click on it | the row opens to its command, its retained output, and its exit status; a click folds it back to one row |
|
|
815
|
+
| a bash command that exits non-zero | the same row as any other failure: the tool's name in the failed colour, with the `exit 1` it ended on |
|
|
816
|
+
| a call that runs for more than a second, watched while it runs | its name is drawn in the running colour with `~1s` counting up at the end of the row, the command still on it; both give way to the measured facts the moment the result lands |
|
|
817
|
+
| a PTC turn with several dispatched calls, one of them slow, one of them exiting non-zero | the call still in flight names itself in the running colour, the one that exited non-zero is red with its `exit 1`, and the rest are plain with their own outcome |
|
|
818
|
+
| a PTC turn whose program ran for more than a second | the card's row counts the seconds while it runs; once the program answers, the total stays on the row dimmed and italic |
|
|
819
|
+
| `dsh-tui: { tokens: { tool.running.title: { hidden: true } } }` in `$DSH_HOME/settings.yaml`, then a slow call | the name is drawn in the colour a settled call uses and the seconds still count, and the call still settles into its result |
|
|
820
|
+
| `dsh-tui: { tokens: { tool.running.elapsed: { hidden: true } } }`, then a slow call | the row keeps its running colour and reports no duration |
|
|
711
821
|
| a call that failed, then a click on its card | the card opens to the reason it failed — the same words the model was shown — and a click folds it back |
|
|
712
822
|
| `dsh-tui: { tools: { bash: { output: tail, tail: 5 } } }`, then a bash run | the folded row keeps the last five output rows and counts the rest |
|
|
713
823
|
| `dsh-tui: { tools: { read: { collapsed: false } } }`, then a read | the card starts open; folded on a narrow terminal, the path gives up room first and the row stops short of the edge |
|
|
@@ -734,6 +844,8 @@ The automated checks drive a real PTY, but they run on this machine's terminal.
|
|
|
734
844
|
| a 40-column terminal | transcript and card rows end in `…` instead of wrapping into the next line |
|
|
735
845
|
| a question with a long option at 40 columns | the option wraps onto rows indented under its label, and `0. other — type your own answer` sits under the list |
|
|
736
846
|
| press `0` on a question, type an answer, press Enter | the editor under row `0` shows the text as it is edited, and the model receives it as that question's answer |
|
|
847
|
+
| a question with no options, then type `/` | no menu opens and the slash stays part of the answer: answers are text the model reads, so the commands this session can run are offered only in the prompt bar |
|
|
848
|
+
| the same question, then type `@` and a fragment | the workspace's files are offered under the answer, and the menu's own keys pick a row instead of answering the question |
|
|
737
849
|
| type a prompt without sending it, then answer a question | the prompt bar steps aside while the question is open and holds the same prompt again afterwards |
|
|
738
850
|
| `echo hi \| dsh --profile tui` | refuses with a non-zero exit and a message naming the TTY requirement |
|
|
739
851
|
| `/stash`, `/stash-pop` in one terminal | the footer shows `stash 1` after the stash and the draft returns to the editor after the pop |
|
|
@@ -758,9 +870,11 @@ The workflow stores no npm token: the registry trusts `release.yml` on the `npm-
|
|
|
758
870
|
|
|
759
871
|
- Two different things are called a preset. The agent mode (`--preset`, `/preset`) is fixed once a session has produced a turn; the permission preset (`/permission <preset>`, named in the status line) can change at any time.
|
|
760
872
|
- `/model` changes the route and reasoning effort for the running session only. Catalog membership is advisory — an adapter may accept an id it does not advertise, while an explicit effort is checked against the route's own levels before it is applied. The picker offers the routes this deployment configured, not the ones it can prove credentialed: a provider whose key or sign-in is still missing appears like any other, and its first request names the missing credential.
|
|
873
|
+
- Model discovery failures show a safe notice. Healthy provider catalogs remain selectable. Check provider configuration and credentials before retrying `/model`.
|
|
761
874
|
- Scrolling is the mouse wheel, or the terminal's own scrollback keys where it offers them.
|
|
762
875
|
- A turn that ran longer than ten seconds rings the terminal bell when it ends, because the reader may have walked away; `--no-bell` turns that off.
|
|
763
876
|
- The dock shows the goal, plan mode, the todo items still to do, and any background job or delegation still running; a settled item leaves rather than turns into a completed row, and the list clears when the next turn opens so a fresh task never inherits the previous one's checklist. The transcript marks where older history was compacted away. `/plan` toggles plan mode; `/plan <message>` also steers that message, which is the base command's own behaviour.
|
|
877
|
+
- The dock shows three running subagents by default. When more than three run, click the heading, overflow row, or blank cells in that section to show all. Click again to show three. Click a child’s text to view its transcript. Press `ctrl+b` to return.
|
|
764
878
|
- Background jobs and subagent runs are live process state, not durable events: they disappear when the run ends, and a resumed session starts with an empty board and roster.
|
|
765
879
|
- A card reads its tool's own render intent through the agent whose session is on screen, so a stored session with no live agent — one this process is not running, or a child that has already finished — folds to the generic card instead of the tool's own.
|
|
766
880
|
- Reading a child's conversation does not move the terminal: commands, approvals, and the status line stay with the session you launched, and the transcript is the only thing that switches. The status line carries the way back, read from the map in force, so a remap shows up without reopening the view.
|
|
@@ -784,7 +898,7 @@ discipline as this one.
|
|
|
784
898
|
| Plugin | What it adds |
|
|
785
899
|
|---|---|
|
|
786
900
|
| [`@sagmans/dsh-auto-compact`](https://github.com/sagmans/dsh-auto-compact) | An absolute token trigger for automatic compaction: the conversation condenses at `min(thresholdTokens, contextWindow × thresholdRatio)` instead of the window ratio alone, so a large-window model pays a fixed price, with per-route overrides. Its patch swaps the shipped `compaction-basic` backend — the same row this bundle already takes out of the global composition — so the profile still keeps exactly one compaction service, and this surface needs no setting for it. |
|
|
787
|
-
| [`@sagmans/dsh-provider-extra`](https://github.com/sagmans/dsh-provider-extra) | Extra provider routes: OpenCode Go, which sends the live conversation id in `x-opencode-session` for routing and prompt caching, and OpenAI Codex over a ChatGPT subscription's OAuth flow with the harness credential store. Its routes join the `/model` picker like every configured provider. |
|
|
901
|
+
| [`@sagmans/dsh-provider-extra`](https://github.com/sagmans/dsh-provider-extra) | Extra provider routes: OpenCode Go, which sends the live conversation id in `x-opencode-session` for routing and prompt caching, and OpenAI Codex over a ChatGPT subscription's OAuth flow with the harness credential store. Its routes join the `/model` picker like every configured provider. TUI also works without this package, using the core `llm` and `agentDefaultModel` services. |
|
|
788
902
|
|
|
789
903
|
```sh
|
|
790
904
|
dsh plugin --profile tui add @sagmans/dsh-auto-compact
|