martty 0.2.11

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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +118 -0
  3. package/bin/dsh-tui.js +68 -0
  4. package/cordis.patch.yml +30 -0
  5. package/creator/cordis.patch.yml +6 -0
  6. package/creator/package.json +10 -0
  7. package/lib/acp-client-events.js +65 -0
  8. package/lib/acp-client.js +114 -0
  9. package/lib/acp-host.js +24 -0
  10. package/lib/acp-session-config.js +376 -0
  11. package/lib/acp-session-plan.js +196 -0
  12. package/lib/acp-session-stats.js +239 -0
  13. package/lib/agent.js +64 -0
  14. package/lib/boot.js +119 -0
  15. package/lib/client-process.js +11 -0
  16. package/lib/client-run.js +379 -0
  17. package/lib/cordis-protocol.js +51 -0
  18. package/lib/creator-overlay.js +77 -0
  19. package/lib/demo-skin.js +79 -0
  20. package/lib/ember.js +20 -0
  21. package/lib/index.js +226 -0
  22. package/lib/inspect.js +971 -0
  23. package/lib/jsonrpc-line-transport.js +155 -0
  24. package/lib/mux.js +281 -0
  25. package/lib/palettes/default.json +44 -0
  26. package/lib/palettes/ember.json +44 -0
  27. package/lib/plan-view.js +92 -0
  28. package/lib/profile-acp-client.js +11 -0
  29. package/lib/right-demo.js +55 -0
  30. package/lib/runner.js +94 -0
  31. package/lib/spawn-tui.js +179 -0
  32. package/lib/stats-view.js +90 -0
  33. package/lib/tui-commands.js +144 -0
  34. package/lib/tui-overlay.js +252 -0
  35. package/lib/tui-slots.js +351 -0
  36. package/lib/tui-theme.js +463 -0
  37. package/package.json +83 -0
  38. package/skills/tui-plugin-development/SKILL.md +172 -0
  39. package/vendor/darwin-arm64/dsh-tui +0 -0
  40. package/vendor/darwin-x64/dsh-tui +0 -0
  41. package/vendor/linux-arm64/dsh-tui +0 -0
  42. package/vendor/linux-x64/dsh-tui +0 -0
  43. package/vendor/win32-x64/dsh-tui.exe +0 -0
package/lib/index.js ADDED
@@ -0,0 +1,226 @@
1
+ /**
2
+ * TUI shell Cordis plugin: spawn the native painter and mux ACP + palettes.
3
+ *
4
+ * Injects `acpClient`, `acpSessionConfig`, the TUI registries, and the sibling
5
+ * `tuiCordisClientRunner`.
6
+ * The shell owns the native painter and only binds transport; it does not
7
+ * evaluate dynamic Client code. Does not import dsh or dsh-acp.
8
+ *
9
+ * dsh-tui --agent dsh-acp
10
+ * dsh-tui --agent dsh --agent-arg --profile --agent-arg acp
11
+ */
12
+
13
+ import { nativeBinary, spawnPluginTui } from './spawn-tui.js'
14
+ import { CORDIS_METHODS } from './cordis-protocol.js'
15
+ import { muxAcpAndCompositor } from './mux.js'
16
+
17
+ export const name = 'dsh-tui-shell'
18
+ export const inject = [
19
+ 'acpClient', 'tuiTheme', 'tuiSlots', 'tuiCommands', 'tuiOverlay',
20
+ 'acpClientEvents', 'acpSessionConfig', 'tuiCordisClientRunner',
21
+ ]
22
+
23
+ const shellStateKey = Symbol.for('martty/shell-state')
24
+
25
+ /**
26
+ * A module reload must not create a second stdin reader. `globalThis` is the
27
+ * ownership boundary of the Node host process; ESM module locals are not,
28
+ * because Cordis can evaluate a fresh module instance during HMR.
29
+ * @type {{
30
+ * livePainter: null | ({ muxed: boolean } & Awaited<ReturnType<typeof spawnPluginTui>>),
31
+ * startingPainter: null | Promise<{ muxed: boolean } & Awaited<ReturnType<typeof spawnPluginTui>>>,
32
+ * refuseRespawn: boolean,
33
+ * }}
34
+ */
35
+ const shellState = globalThis[shellStateKey] ??= {
36
+ livePainter: null,
37
+ startingPainter: null,
38
+ refuseRespawn: false,
39
+ }
40
+
41
+ /** Drop the sticky painter so tests can apply() more than once in-process. */
42
+ export function resetShellForTests() {
43
+ shellState.livePainter = null
44
+ shellState.startingPainter = null
45
+ shellState.refuseRespawn = false
46
+ }
47
+
48
+ /**
49
+ * Fiber dispose (HMR / config-watch recompose) SIGTERM is not a user quit.
50
+ * A normal exit or SIGINT is: the host must give the TTY back.
51
+ * @param {NodeJS.Signals | null} signal
52
+ * @returns {boolean}
53
+ */
54
+ export function shouldQuitOnPainterExit(signal) {
55
+ return signal !== 'SIGTERM'
56
+ }
57
+
58
+ /** Child raw-mode / alt-screen is the same TTY. Reset it if the painter died dirty. */
59
+ export function restoreHostTty() {
60
+ if (process.stdin.isTTY) {
61
+ try {
62
+ process.stdin.setRawMode(false)
63
+ } catch {
64
+ // stdin already closed
65
+ }
66
+ }
67
+ if (process.stdout.isTTY) {
68
+ process.stdout.write(
69
+ '\x1b[?2004l\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?25h\x1b[?1049l',
70
+ )
71
+ }
72
+ }
73
+
74
+ /**
75
+ * @param {object} ctx
76
+ * @param {{ extraArgs?: string[] }} [options]
77
+ */
78
+ export function apply(ctx, options = {}) {
79
+ return applyShell(ctx, options)
80
+ }
81
+
82
+ /**
83
+ * @param {object} ctx
84
+ * @param {{ extraArgs?: string[] }} [options]
85
+ */
86
+ export async function applyShell(ctx, options = {}) {
87
+ const handle = ctx.tuiTheme ?? ctx.get?.('tuiTheme')
88
+ if (handle === undefined || typeof handle.bindNotify !== 'function'
89
+ || typeof handle.observeSelected !== 'function') {
90
+ throw new Error('dsh-tui-shell: ctx.tuiTheme must expose bindNotify and observeSelected')
91
+ }
92
+ const slots = ctx.tuiSlots ?? ctx.get?.('tuiSlots')
93
+ if (slots === undefined || typeof slots.bindNotify !== 'function') {
94
+ throw new Error('dsh-tui-shell: ctx.tuiSlots must expose bindNotify')
95
+ }
96
+ const commands = ctx.tuiCommands ?? ctx.get?.('tuiCommands')
97
+ if (commands === undefined || typeof commands.bindNotify !== 'function'
98
+ || typeof commands.dispatch !== 'function') {
99
+ throw new Error('dsh-tui-shell: ctx.tuiCommands must expose bindNotify and dispatch')
100
+ }
101
+ const overlay = ctx.tuiOverlay ?? ctx.get?.('tuiOverlay')
102
+ if (overlay === undefined || typeof overlay.bindNotify !== 'function'
103
+ || typeof overlay.dispatch !== 'function') {
104
+ throw new Error('dsh-tui-shell: ctx.tuiOverlay must expose bindNotify and dispatch')
105
+ }
106
+ const agent = ctx.acpClient ?? ctx.get?.('acpClient')
107
+ if (agent === undefined || agent.stdin === undefined || agent.stdout === undefined) {
108
+ throw new Error('dsh-tui-shell: ctx.acpClient must expose stdin and stdout')
109
+ }
110
+ const clientRunner = ctx.tuiCordisClientRunner ?? ctx.get?.('tuiCordisClientRunner')
111
+ if (clientRunner === undefined || typeof clientRunner.bindTransport !== 'function'
112
+ || typeof clientRunner.selectTheme !== 'function') {
113
+ throw new Error(
114
+ 'dsh-tui-shell: ctx.tuiCordisClientRunner must expose bindTransport and selectTheme',
115
+ )
116
+ }
117
+ const sessionConfig = ctx.acpSessionConfig ?? ctx.get?.('acpSessionConfig')
118
+ if (sessionConfig === undefined || typeof sessionConfig.bindTransport !== 'function'
119
+ || typeof sessionConfig.observeClient !== 'function'
120
+ || typeof sessionConfig.observeAgent !== 'function') {
121
+ throw new Error(
122
+ 'dsh-tui-shell: ctx.acpSessionConfig must expose bindTransport and ACP observers',
123
+ )
124
+ }
125
+ const clientEvents = ctx.acpClientEvents ?? ctx.get?.('acpClientEvents')
126
+ if (clientEvents === undefined || typeof clientEvents.observeClient !== 'function'
127
+ || typeof clientEvents.observeAgent !== 'function') {
128
+ throw new Error(
129
+ 'dsh-tui-shell: ctx.acpClientEvents must expose ACP observers',
130
+ )
131
+ }
132
+
133
+ // Config-watch recomposes the tree at boot and disposes this fiber. Keep
134
+ // one painter; do not kill it from ctx.effect or the screen never appears.
135
+ // After a user quit, do not spawn a second empty TUI on the same TTY.
136
+ if (shellState.refuseRespawn) {
137
+ return
138
+ }
139
+ let connection = shellState.livePainter
140
+ if (connection === null || connection.child.exitCode !== null) {
141
+ if (shellState.startingPainter === null) {
142
+ shellState.startingPainter = (async () => {
143
+ const bin = nativeBinary()
144
+ const started = await spawnPluginTui(
145
+ bin,
146
+ options.extraArgs ?? [],
147
+ options.tty,
148
+ )
149
+ const live = { ...started, muxed: false }
150
+ shellState.livePainter = live
151
+ const { child } = live
152
+ child.on('exit', (code, signal) => {
153
+ try {
154
+ agent.child?.kill('SIGTERM')
155
+ } catch {
156
+ // already gone
157
+ }
158
+ restoreHostTty()
159
+ if (!shouldQuitOnPainterExit(signal)) return
160
+ shellState.refuseRespawn = true
161
+ if (shellState.livePainter?.child === child) shellState.livePainter = null
162
+ process.exit(code ?? (signal === 'SIGINT' ? 130 : 0))
163
+ })
164
+ child.on('error', (err) => {
165
+ console.error(`dsh-tui: ${err.message}`)
166
+ process.exit(1)
167
+ })
168
+ process.once('exit', () => {
169
+ try {
170
+ child.kill('SIGTERM')
171
+ } catch {
172
+ // already gone
173
+ }
174
+ })
175
+ return live
176
+ })()
177
+ }
178
+ const starting = shellState.startingPainter
179
+ try {
180
+ connection = await starting
181
+ } finally {
182
+ if (shellState.startingPainter === starting) shellState.startingPainter = null
183
+ }
184
+ }
185
+
186
+ if (!connection.muxed) {
187
+ const mux = muxAcpAndCompositor({
188
+ agent,
189
+ tui: connection,
190
+ onHost(message) {
191
+ clientRunner.onHost(message)
192
+ },
193
+ onCordisReady() {
194
+ clientRunner.sync()
195
+ },
196
+ onAcp(direction, message) {
197
+ if (direction === 'client') {
198
+ clientEvents.observeClient(message)
199
+ } else {
200
+ clientEvents.observeAgent(message)
201
+ }
202
+ },
203
+ onCompositor(message) {
204
+ if (message.method === CORDIS_METHODS.themeSelected) {
205
+ return clientRunner.selectTheme(message.params)
206
+ }
207
+ if (message.method === CORDIS_METHODS.commandInvoke) {
208
+ return commands.dispatch(message.params)
209
+ }
210
+ if (message.method === CORDIS_METHODS.overlayEvent) {
211
+ return overlay.dispatch(message.params)
212
+ }
213
+ throw new Error(`unsupported Cordis TUI method: ${String(message.method)}`)
214
+ },
215
+ })
216
+ handle.bindNotify((method, params) => mux.notifyTui(method, params))
217
+ slots.bindNotify((method, params) => mux.notifyTui(method, params))
218
+ commands.bindNotify((method, params) => mux.notifyTui(method, params))
219
+ overlay.bindNotify((method, params) => mux.notifyTui(method, params))
220
+ clientRunner.bindTransport(mux.requestAgent)
221
+ sessionConfig.bindTransport(mux.requestTui)
222
+ connection.resume?.()
223
+ connection.muxed = true
224
+ }
225
+ // Cordis collects apply's return as an effect. Only a disposer or nothing.
226
+ }