martty 0.2.17 → 0.2.18

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/lib/acp-host.js CHANGED
@@ -1,24 +1,36 @@
1
1
  /** Resolve the embeddable ACP plugin from TUI's own dependency graph. */
2
2
 
3
3
  import { createRequire } from 'node:module'
4
+ import { pathToFileURL } from 'node:url'
4
5
 
5
6
  export const name = 'dsh-tui-acp-host'
6
7
  export const inject = ['loader']
7
8
 
8
- const requireFromTui = createRequire(import.meta.url)
9
-
10
- function ownAcpPlugin() {
11
- const specifier = '@openma/deepseek-harness-acp/plugin'
12
- try {
13
- return requireFromTui.resolve(specifier)
14
- } catch {
15
- // A source-linked package may rely on the active profile's installation.
16
- return specifier
9
+ function resolvedModule(ctx, specifier) {
10
+ for (const anchor of [ctx.baseUrl, import.meta.url]) {
11
+ if (typeof anchor !== 'string') continue
12
+ try {
13
+ return pathToFileURL(createRequire(anchor).resolve(specifier)).href
14
+ } catch {
15
+ // Try TUI's dependency graph after the active profile.
16
+ }
17
17
  }
18
+ return specifier
18
19
  }
19
20
 
20
21
  export async function apply(ctx, config) {
21
- const exports = await ctx.loader.import(ownAcpPlugin())
22
+ const specifier = '@openma/deepseek-harness-acp/plugin'
23
+ const resolved = resolvedModule(ctx, specifier)
24
+ let exports
25
+ try {
26
+ exports = await import(resolved)
27
+ } catch (cause) {
28
+ throw new Error(`dsh-tui: failed to import ${specifier} from ${resolved}`, { cause })
29
+ }
22
30
  const plugin = ctx.loader.unwrapExports(exports)
23
- await ctx.plugin(plugin, config)
31
+ try {
32
+ await ctx.plugin(plugin, config)
33
+ } catch (cause) {
34
+ throw new Error(`dsh-tui: failed to mount ${specifier}`, { cause })
35
+ }
24
36
  }
package/lib/ayu.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Gallery palette pack `ayu`. Registers complete token maps: dark from the
3
+ * Ayu dark variant, light from Ayu Light
4
+ * (terminalcolors.com/themes/ayu). Does not activate:
5
+ * `/theme` covers it. `inject = ['tuiTheme']`: sibling profile row, not
6
+ * `ctx.plugin` inside the runner.
7
+ */
8
+
9
+ import { readFileSync } from 'node:fs'
10
+
11
+ const ayuPalette = JSON.parse(
12
+ readFileSync(new URL('./palettes/ayu.json', import.meta.url), 'utf8'),
13
+ )
14
+
15
+ export const name = 'tui-theme-ayu'
16
+ export const inject = ['tuiTheme']
17
+
18
+ export function apply(ctx) {
19
+ ctx.effect(() => ctx.tuiTheme.register(ayuPalette, { activate: false }))
20
+ }
21
+
22
+ export { ayuPalette }
package/lib/boot.js CHANGED
@@ -15,6 +15,12 @@ import { apply as applyShell } from './index.js'
15
15
  import { resolveStackedAgent } from './agent.js'
16
16
  import { apply as applySlots } from './tui-slots.js'
17
17
  import { apply as applyTheme } from './tui-theme.js'
18
+ import { apply as applyAyu, inject as ayuInject } from './ayu.js'
19
+ import { apply as applyCatppuccin, inject as catppuccinInject } from './catppuccin.js'
20
+ import { apply as applyKanagawa, inject as kanagawaInject } from './kanagawa.js'
21
+ import { apply as applyEverforest, inject as everforestInject } from './everforest.js'
22
+ import { apply as applyIceberg, inject as icebergInject } from './iceberg.js'
23
+ import { apply as applySolarized, inject as solarizedInject } from './solarized.js'
18
24
  import { apply as applyCommands } from './tui-commands.js'
19
25
  import { apply as applyOverlay } from './tui-overlay.js'
20
26
  import { apply as applyPresets, inject as presetsInject } from './tui-presets.js'
@@ -34,7 +40,7 @@ import { installTuiLocalPlugins } from './tui-local-plugins.js'
34
40
  * @param {{ stdin: number | 'inherit', stdout: number | 'inherit' }} [options.tty]
35
41
  * @param {string} [options.settingsPath]
36
42
  * @param {string} [options.artifactRoot]
37
- * @param {Array<{ id: string, kind: 'theme' | 'ui-preset', entry: string }>} [options.packagePlugins]
43
+ * @param {Array<{ id: string, kind: 'theme' | 'ui', entry: string }>} [options.packagePlugins]
38
44
  */
39
45
  export async function bootClient(options = {}) {
40
46
  const { Context } = await import('@deepseek-ai/cordis')
@@ -49,6 +55,12 @@ export async function bootClient(options = {}) {
49
55
  const presetConfig = { settingsPath }
50
56
  if (typeof ctx.plugin === 'function') {
51
57
  await ctx.plugin({ name: 'tui-theme', inject: [], apply: applyTheme }, presetConfig)
58
+ await ctx.plugin({ name: 'tui-theme-ayu', inject: ayuInject, apply: applyAyu })
59
+ await ctx.plugin({ name: 'tui-theme-catppuccin', inject: catppuccinInject, apply: applyCatppuccin })
60
+ await ctx.plugin({ name: 'tui-theme-kanagawa', inject: kanagawaInject, apply: applyKanagawa })
61
+ await ctx.plugin({ name: 'tui-theme-everforest', inject: everforestInject, apply: applyEverforest })
62
+ await ctx.plugin({ name: 'tui-theme-iceberg', inject: icebergInject, apply: applyIceberg })
63
+ await ctx.plugin({ name: 'tui-theme-solarized', inject: solarizedInject, apply: applySolarized })
52
64
  await ctx.plugin({ name: 'tui-slots', inject: [], apply: applySlots })
53
65
  await ctx.plugin({ name: 'tui-commands', inject: [], apply: applyCommands })
54
66
  await ctx.plugin({ name: 'tui-overlay', inject: [], apply: applyOverlay })
@@ -87,7 +99,7 @@ export async function bootClient(options = {}) {
87
99
  name: 'dsh-tui-shell',
88
100
  inject: [
89
101
  'acpClient', 'tuiTheme', 'tuiSlots', 'tuiCommands', 'tuiOverlay',
90
- 'acpClientEvents', 'acpSessionConfig', 'tuiCordisClientRunner',
102
+ 'tuiPresets', 'acpClientEvents', 'acpSessionConfig', 'tuiCordisClientRunner',
91
103
  ],
92
104
  apply: applyShell,
93
105
  },
@@ -95,6 +107,12 @@ export async function bootClient(options = {}) {
95
107
  )
96
108
  } else {
97
109
  applyTheme(ctx, presetConfig)
110
+ applyAyu(ctx)
111
+ applyCatppuccin(ctx)
112
+ applyKanagawa(ctx)
113
+ applyEverforest(ctx)
114
+ applyIceberg(ctx)
115
+ applySolarized(ctx)
98
116
  applySlots(ctx)
99
117
  applyCommands(ctx)
100
118
  applyOverlay(ctx)
@@ -145,7 +163,7 @@ export function parseClientPluginsEnv(value) {
145
163
  if (typeof entry.id !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(entry.id)) {
146
164
  throw new Error(`dsh-tui: installed Client plugin entry ${index} has an invalid id`)
147
165
  }
148
- if (!['theme', 'ui-preset'].includes(entry.kind)) {
166
+ if (!['theme', 'ui', 'ui-preset'].includes(entry.kind)) {
149
167
  throw new Error(`dsh-tui: installed Client plugin entry ${index} has an invalid kind`)
150
168
  }
151
169
  if (typeof entry.entry !== 'string') {
@@ -160,7 +178,11 @@ export function parseClientPluginsEnv(value) {
160
178
  if (url.protocol !== 'file:') {
161
179
  throw new Error(`dsh-tui: installed Client plugin entry ${index} must use a file URL`)
162
180
  }
163
- return { id: entry.id, kind: entry.kind, entry: url.href }
181
+ return {
182
+ id: entry.id,
183
+ kind: entry.kind === 'ui-preset' ? 'ui' : entry.kind,
184
+ entry: url.href,
185
+ }
164
186
  })
165
187
  }
166
188
 
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Gallery palette pack `catppuccin`. Registers complete token maps: dark
3
+ * from the Catppuccin Mocha variant, light from Catppuccin Latte
4
+ * (terminalcolors.com/themes/catppuccin)
5
+ * (terminalcolors.com/themes/catppuccin). Does not activate: `/theme`
6
+ * covers it. `inject = ['tuiTheme']`: sibling profile row, not `ctx.plugin`
7
+ * inside the runner.
8
+ */
9
+
10
+ import { readFileSync } from 'node:fs'
11
+
12
+ const catppuccinPalette = JSON.parse(
13
+ readFileSync(new URL('./palettes/catppuccin.json', import.meta.url), 'utf8'),
14
+ )
15
+
16
+ export const name = 'tui-theme-catppuccin'
17
+ export const inject = ['tuiTheme']
18
+
19
+ export function apply(ctx) {
20
+ ctx.effect(() => ctx.tuiTheme.register(catppuccinPalette, { activate: false }))
21
+ }
22
+
23
+ export { catppuccinPalette }
package/lib/client-run.js CHANGED
@@ -161,7 +161,10 @@ function restrictedCtx(env, own, inject) {
161
161
  && typeof sourceTheme.registerOwned === 'function'
162
162
  ? sourceTheme.registerOwned.bind(sourceTheme, env.pluginId)
163
163
  : sourceTheme.register.bind(sourceTheme)
164
- const registration = register(palette, options)
164
+ const registration = register(palette, {
165
+ ...options,
166
+ ...(typeof env.pluginSource === 'string' ? { source: env.pluginSource } : {}),
167
+ })
165
168
  const dispose = own(
166
169
  typeof registration?.dispose === 'function'
167
170
  ? registration.dispose.bind(registration)
@@ -199,7 +202,13 @@ function restrictedCtx(env, own, inject) {
199
202
  ? undefined
200
203
  : {
201
204
  register(options, mount) {
202
- return own(sourcePresets.register(options, mount))
205
+ const register = typeof env.pluginId === 'string'
206
+ && typeof sourcePresets.registerOwned === 'function'
207
+ ? sourcePresets.registerOwned.bind(sourcePresets, env.pluginId)
208
+ : sourcePresets.register.bind(sourcePresets)
209
+ return own(register(options, mount, {
210
+ ...(typeof env.pluginSource === 'string' ? { source: env.pluginSource } : {}),
211
+ }))
203
212
  },
204
213
  list() {
205
214
  return sourcePresets.list()
@@ -21,6 +21,10 @@ export const CORDIS_METHODS = Object.freeze({
21
21
  pluginStart: '_dsh/cordis/plugins/start',
22
22
  pluginStop: '_dsh/cordis/plugins/stop',
23
23
  pluginRetract: '_dsh/cordis/plugins/retract',
24
+ approvalsUpdate: '_dsh/cordis/tui/approvals/update',
25
+ approvalRespond: '_dsh/cordis/tui/approvals/respond',
26
+ uiUpdate: '_dsh/cordis/tui/ui/update',
27
+ uiSelected: '_dsh/cordis/tui/ui/selected',
24
28
  themeUpdate: '_dsh/cordis/tui/theme/update',
25
29
  themeRemove: '_dsh/cordis/tui/theme/remove',
26
30
  themeSelected: '_dsh/cordis/tui/theme/selected',
@@ -41,7 +41,7 @@ function registerArtifactTools(ctx, scopedCtx, defineTool, store) {
41
41
  name: 'tui_plugin_save',
42
42
  description:
43
43
  'Persist one successfully activated, Client-only Cordis Package as a durable TUI artifact. '
44
- + 'Use this only after cordis_run succeeds. UI Presets and Theme Plugins survive restart only '
44
+ + 'Use this only after cordis_run succeeds. UI and Theme Plugins survive restart only '
45
45
  + 'after this Tool returns saved. Replacing an existing artifact must be explicit.',
46
46
  parameters: {
47
47
  artifactId: {
@@ -52,7 +52,7 @@ function registerArtifactTools(ctx, scopedCtx, defineTool, store) {
52
52
  kind: {
53
53
  type: 'string',
54
54
  required: true,
55
- enum: ['ui-preset', 'theme'],
55
+ enum: ['ui', 'theme'],
56
56
  description: 'Durable TUI contribution kind.',
57
57
  },
58
58
  pluginId: {
@@ -86,7 +86,7 @@ function registerArtifactTools(ctx, scopedCtx, defineTool, store) {
86
86
  if (typeof source.code?.host === 'string') {
87
87
  throw new Error(
88
88
  'tui_plugin_save: durable TUI artifacts are client-only; a Package with a Host half '
89
- + 'must be shipped as a standard profile plugin instead',
89
+ + 'belongs to the connected Harness and needs its separate shipping path',
90
90
  )
91
91
  }
92
92
  if (typeof source.code?.client !== 'string') {
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Gallery palette pack `everforest`. Registers complete dark/light token maps:
3
+ * dark from Everforest Dark, light from Everforest Light (terminalcolors.com/themes/everforest). Does not activate:
4
+ * `/theme` covers it. `inject = ['tuiTheme']`: sibling profile row, not
5
+ * `ctx.plugin` inside the runner.
6
+ */
7
+
8
+ import { readFileSync } from 'node:fs'
9
+
10
+ const everforestPalette = JSON.parse(
11
+ readFileSync(new URL('./palettes/everforest.json', import.meta.url), 'utf8'),
12
+ )
13
+
14
+ export const name = 'tui-theme-everforest'
15
+ export const inject = ['tuiTheme']
16
+
17
+ export function apply(ctx) {
18
+ ctx.effect(() => ctx.tuiTheme.register(everforestPalette, { activate: false }))
19
+ }
20
+
21
+ export { everforestPalette }
package/lib/iceberg.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Gallery palette pack `iceberg`. Registers complete dark/light token maps:
3
+ * dark from Iceberg Dark, light from Iceberg Light (terminalcolors.com/themes/iceberg). Does not activate:
4
+ * `/theme` covers it. `inject = ['tuiTheme']`: sibling profile row, not
5
+ * `ctx.plugin` inside the runner.
6
+ */
7
+
8
+ import { readFileSync } from 'node:fs'
9
+
10
+ const icebergPalette = JSON.parse(
11
+ readFileSync(new URL('./palettes/iceberg.json', import.meta.url), 'utf8'),
12
+ )
13
+
14
+ export const name = 'tui-theme-iceberg'
15
+ export const inject = ['tuiTheme']
16
+
17
+ export function apply(ctx) {
18
+ ctx.effect(() => ctx.tuiTheme.register(icebergPalette, { activate: false }))
19
+ }
20
+
21
+ export { icebergPalette }
package/lib/index.js CHANGED
@@ -16,7 +16,7 @@ import { muxAcpAndCompositor } from './mux.js'
16
16
 
17
17
  export const name = 'dsh-tui-shell'
18
18
  export const inject = [
19
- 'acpClient', 'tuiTheme', 'tuiSlots', 'tuiCommands', 'tuiOverlay',
19
+ 'acpClient', 'tuiTheme', 'tuiPresets', 'tuiSlots', 'tuiCommands', 'tuiOverlay',
20
20
  'acpClientEvents', 'acpSessionConfig', 'tuiCordisClientRunner',
21
21
  ]
22
22
 
@@ -115,6 +115,7 @@ export async function applyShell(ctx, options = {}) {
115
115
  throw new Error('dsh-tui-shell: ctx.acpClient must expose stdin and stdout')
116
116
  }
117
117
  const clientRunner = ctx.tuiCordisClientRunner ?? ctx.get?.('tuiCordisClientRunner')
118
+ const presets = ctx.tuiPresets ?? ctx.get?.('tuiPresets')
118
119
  if (clientRunner === undefined || typeof clientRunner.bindTransport !== 'function'
119
120
  || typeof clientRunner.selectTheme !== 'function') {
120
121
  throw new Error(
@@ -225,18 +226,25 @@ export async function applyShell(ctx, options = {}) {
225
226
  if (message.method === CORDIS_METHODS.overlayEvent) {
226
227
  return overlay.dispatch(message.params)
227
228
  }
229
+ if (message.method === CORDIS_METHODS.approvalRespond) {
230
+ return clientRunner.respondApproval(message.params)
231
+ }
232
+ if (message.method === CORDIS_METHODS.uiSelected) {
233
+ return clientRunner.selectUi(message.params)
234
+ }
228
235
  throw new Error(`unsupported Cordis TUI method: ${String(message.method)}`)
229
236
  },
230
237
  })
231
238
  const notifyTui = (method, params) => mux.notifyTui(method, params)
232
239
  republishCompositorState = () => {
233
240
  handle.bindNotify(notifyTui)
241
+ presets?.bindNotify?.(notifyTui)
234
242
  slots.bindNotify(notifyTui)
235
243
  commands.bindNotify(notifyTui)
236
244
  overlay.bindNotify(notifyTui)
237
245
  }
238
246
  republishCompositorState()
239
- clientRunner.bindTransport(mux.requestAgent)
247
+ clientRunner.bindTransport(mux.requestAgent, notifyTui)
240
248
  sessionConfig.bindTransport(mux.requestTui)
241
249
  connection.resume?.()
242
250
  connection.muxed = true
package/lib/inspect.js CHANGED
@@ -756,14 +756,15 @@ async function mountClientHalf(
756
756
  * acpSessionStatus?: object,
757
757
  * localPlugins?: object,
758
758
  * requestAgent: (method: string, params?: object) => Promise<unknown>,
759
+ * notifyTui?: (method: string, params?: object) => void,
759
760
  * }} opts
760
- * @returns {{ onHost: (message: object) => void }}
761
+ * @returns {{ onHost: (message: object) => void, respondApproval: (params: object) => Promise<void> }}
761
762
  */
762
763
  export function attachTuiClient(opts) {
763
764
  const {
764
765
  ctx, tuiTheme, tuiPresets, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
765
766
  acpSessionStats, acpSessionStatus, localPlugins,
766
- requestAgent,
767
+ requestAgent, notifyTui = () => {},
767
768
  } = opts
768
769
  const providers = [
769
770
  themeInspectProvider(tuiTheme),
@@ -780,6 +781,23 @@ export function attachTuiClient(opts) {
780
781
  /** @type {Map<string, () => void>} */
781
782
  const loaded = new Map()
782
783
  const pendingThemeSelections = new Map()
784
+ const pendingUiSelections = new Map()
785
+ const pendingApprovals = new Map()
786
+
787
+ function publishApprovals() {
788
+ notifyTui(CORDIS_METHODS.approvalsUpdate, {
789
+ protocol: 0,
790
+ approvals: [...pendingApprovals.values()].map((request) => ({
791
+ requestId: request.requestId,
792
+ agentId: request.agentId,
793
+ pluginId: request.pluginId,
794
+ packageId: request.packageId,
795
+ mode: request.mode,
796
+ name: request.name,
797
+ purpose: request.purpose,
798
+ })),
799
+ })
800
+ }
783
801
 
784
802
  async function stopOwner(agentId, pluginId) {
785
803
  if (localPlugins?.has?.(pluginId) === true) {
@@ -828,7 +846,7 @@ export function attachTuiClient(opts) {
828
846
  await requestAgent(CORDIS_METHODS.inspectResolve, { agentId, requestId, resolution }).catch(() => {})
829
847
  }
830
848
 
831
- async function runClient(params, direct = false) {
849
+ async function runClient(params, direct = false, approveFutureVersions = false) {
832
850
  const request = params !== null && typeof params === 'object' ? params : {}
833
851
  const requestId = request.requestId
834
852
  if (!direct && typeof requestId !== 'string') return
@@ -842,6 +860,7 @@ export function attachTuiClient(opts) {
842
860
  ? { ok: true, pluginRunId: request.pluginRunId }
843
861
  : undefined
844
862
  const previousThemeOwner = tuiTheme.owner?.(tuiTheme.active())
863
+ const previousUiOwner = tuiPresets?.owner?.(tuiPresets.active?.())
845
864
  try {
846
865
  if (!direct) {
847
866
  host = await requestAgent(CORDIS_METHODS.runHost, {
@@ -850,7 +869,7 @@ export function attachTuiClient(opts) {
850
869
  packageId: request.packageId,
851
870
  mode: request.mode,
852
871
  requestId,
853
- approveFutureVersions: false,
872
+ approveFutureVersions,
854
873
  })
855
874
  }
856
875
  if (host === null || typeof host !== 'object' || host.ok !== true) {
@@ -909,6 +928,15 @@ export function attachTuiClient(opts) {
909
928
  },
910
929
  )
911
930
  loaded.set(request.pluginId, applied.dispose)
931
+ const selectedUi = pendingUiSelections.get(request.pluginId)
932
+ if (selectedUi !== undefined && tuiPresets?.isLoaded?.(selectedUi) === true) {
933
+ pendingUiSelections.delete(request.pluginId)
934
+ tuiPresets.activate(selectedUi)
935
+ if (previousUiOwner !== undefined && previousUiOwner !== request.pluginId
936
+ && tuiTheme.owner?.(tuiTheme.active()) !== previousUiOwner) {
937
+ await stopOwner(request.agentId, previousUiOwner)
938
+ }
939
+ }
912
940
  const selectedTheme = pendingThemeSelections.get(request.pluginId)
913
941
  if (selectedTheme !== undefined && tuiTheme.isLoaded?.(selectedTheme) === true) {
914
942
  pendingThemeSelections.delete(request.pluginId)
@@ -953,7 +981,21 @@ export function attachTuiClient(opts) {
953
981
  case CORDIS_METHODS.inspectQuery:
954
982
  return answerQuery(message.params)
955
983
  case CORDIS_METHODS.requestRun:
984
+ if (message.params !== null && typeof message.params === 'object'
985
+ && message.params.requiresApproval === true
986
+ && typeof message.params.requestId === 'string') {
987
+ pendingApprovals.set(message.params.requestId, { ...message.params })
988
+ publishApprovals()
989
+ return
990
+ }
956
991
  return runClient(message.params)
992
+ case CORDIS_METHODS.requestRunResolved: {
993
+ const requestId = message.params !== null && typeof message.params === 'object'
994
+ ? message.params.requestId
995
+ : undefined
996
+ if (typeof requestId === 'string' && pendingApprovals.delete(requestId)) publishApprovals()
997
+ return
998
+ }
957
999
  case CORDIS_METHODS.userRun:
958
1000
  return runClient(message.params, true)
959
1001
  case CORDIS_METHODS.pluginRetract: {
@@ -970,6 +1012,29 @@ export function attachTuiClient(opts) {
970
1012
  }
971
1013
  }
972
1014
 
1015
+ async function respondApproval(params) {
1016
+ const request = params !== null && typeof params === 'object' ? params : {}
1017
+ if (request.protocol !== 0) throw new Error('unsupported approval response payload')
1018
+ if (typeof request.requestId !== 'string' || request.requestId.length === 0) {
1019
+ throw new Error('approval response needs requestId')
1020
+ }
1021
+ if (!['allow-version', 'allow-future', 'reject'].includes(request.decision)) {
1022
+ throw new Error('approval response has an invalid decision')
1023
+ }
1024
+ const pending = pendingApprovals.get(request.requestId)
1025
+ if (pending === undefined) return
1026
+ pendingApprovals.delete(request.requestId)
1027
+ publishApprovals()
1028
+ if (request.decision === 'reject') {
1029
+ await requestAgent(CORDIS_METHODS.resolveRequestRun, {
1030
+ requestId: request.requestId,
1031
+ resolution: { ok: false, reason: 'rejected' },
1032
+ })
1033
+ return
1034
+ }
1035
+ await runClient(pending, false, request.decision === 'allow-future')
1036
+ }
1037
+
973
1038
  async function selectTheme(params) {
974
1039
  const request = params !== null && typeof params === 'object' ? params : {}
975
1040
  if (request.protocol !== 0) throw new Error('unsupported theme selection payload')
@@ -1042,12 +1107,71 @@ export function attachTuiClient(opts) {
1042
1107
  }
1043
1108
  }
1044
1109
 
1110
+ async function selectUi(params) {
1111
+ const request = params !== null && typeof params === 'object' ? params : {}
1112
+ if (request.protocol !== 0) throw new Error('unsupported UI selection payload')
1113
+ if (typeof request.agentId !== 'string' || request.agentId.length === 0) {
1114
+ throw new Error('UI selection needs agentId')
1115
+ }
1116
+ if (typeof request.id !== 'string' || request.id.length === 0) {
1117
+ throw new Error('UI selection needs id')
1118
+ }
1119
+ if (!tuiPresets?.list?.().some((entry) => entry.id === request.id)) {
1120
+ throw new Error(`UI Plugin "${request.id}" is not registered`)
1121
+ }
1122
+ const previousId = tuiPresets.active()
1123
+ const previousOwner = tuiPresets.owner?.(previousId)
1124
+ const targetOwner = tuiPresets.owner?.(request.id)
1125
+
1126
+ if (targetOwner === undefined || tuiPresets.isLoaded?.(request.id) === true) {
1127
+ tuiPresets.activate(request.id)
1128
+ if (previousOwner !== undefined && previousOwner !== targetOwner
1129
+ && tuiTheme.owner?.(tuiTheme.active()) !== previousOwner) {
1130
+ await stopOwner(request.agentId, previousOwner)
1131
+ }
1132
+ return { ok: true, status: 'selected', id: request.id }
1133
+ }
1134
+ if (localPlugins?.has?.(targetOwner) === true) {
1135
+ await localPlugins.start(targetOwner)
1136
+ if (tuiPresets.isLoaded?.(request.id) !== true) {
1137
+ throw new Error(`local UI Plugin "${targetOwner}" did not load UI "${request.id}"`)
1138
+ }
1139
+ tuiPresets.activate(request.id)
1140
+ if (previousOwner !== undefined && previousOwner !== targetOwner
1141
+ && tuiTheme.owner?.(tuiTheme.active()) !== previousOwner) {
1142
+ await stopOwner(request.agentId, previousOwner)
1143
+ }
1144
+ return { ok: true, status: 'selected', id: request.id }
1145
+ }
1146
+
1147
+ pendingUiSelections.set(targetOwner, request.id)
1148
+ try {
1149
+ const started = await requestAgent(CORDIS_METHODS.pluginStart, {
1150
+ agentId: request.agentId,
1151
+ pluginId: targetOwner,
1152
+ })
1153
+ if (started?.ok === false) {
1154
+ throw new Error(typeof started.message === 'string'
1155
+ ? started.message
1156
+ : `failed to start UI Plugin "${targetOwner}"`)
1157
+ }
1158
+ return { ok: true, status: 'starting', id: request.id }
1159
+ } catch (error) {
1160
+ pendingUiSelections.delete(targetOwner)
1161
+ throw error
1162
+ }
1163
+ }
1164
+
1045
1165
  function dispose() {
1046
1166
  for (const release of loaded.values()) release()
1047
1167
  loaded.clear()
1168
+ if (pendingApprovals.size > 0) {
1169
+ pendingApprovals.clear()
1170
+ publishApprovals()
1171
+ }
1048
1172
  }
1049
1173
 
1050
- return { onHost, selectTheme, sync, dispose }
1174
+ return { onHost, respondApproval, selectTheme, selectUi, sync, dispose }
1051
1175
  }
1052
1176
 
1053
1177
  /** Mount the TUI counterpart of the Web Cordis Client runner. */
@@ -1064,12 +1188,12 @@ export function apply(ctx) {
1064
1188
  const localPlugins = ctx.get?.('tuiLocalPlugins')
1065
1189
  let client
1066
1190
  const service = {
1067
- bindTransport(requestAgent) {
1191
+ bindTransport(requestAgent, notifyTui) {
1068
1192
  client?.dispose()
1069
1193
  client = attachTuiClient({
1070
1194
  ctx, tuiTheme, tuiPresets, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
1071
1195
  acpSessionStats, acpSessionStatus, localPlugins,
1072
- requestAgent,
1196
+ requestAgent, notifyTui,
1073
1197
  })
1074
1198
  const attached = client
1075
1199
  return () => {
@@ -1085,6 +1209,14 @@ export function apply(ctx) {
1085
1209
  if (client === undefined) throw new Error('TUI Cordis Client runner is not attached')
1086
1210
  return client.selectTheme(params)
1087
1211
  },
1212
+ selectUi(params) {
1213
+ if (client === undefined) throw new Error('TUI Cordis Client runner is not attached')
1214
+ return client.selectUi(params)
1215
+ },
1216
+ respondApproval(params) {
1217
+ if (client === undefined) throw new Error('TUI Cordis Client runner is not attached')
1218
+ return client.respondApproval(params)
1219
+ },
1088
1220
  sync() {
1089
1221
  client?.sync()
1090
1222
  },
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Gallery palette pack `kanagawa`. Registers complete dark/light token maps:
3
+ * dark from the Kanagawa Wave variant, light from Kanagawa Lotus
4
+ * (terminalcolors.com/themes/kanagawa). Does not activate: `/theme` covers
5
+ * it. `inject = ['tuiTheme']`: sibling profile row, not `ctx.plugin` inside
6
+ * the runner.
7
+ */
8
+
9
+ import { readFileSync } from 'node:fs'
10
+
11
+ const kanagawaPalette = JSON.parse(
12
+ readFileSync(new URL('./palettes/kanagawa.json', import.meta.url), 'utf8'),
13
+ )
14
+
15
+ export const name = 'tui-theme-kanagawa'
16
+ export const inject = ['tuiTheme']
17
+
18
+ export function apply(ctx) {
19
+ ctx.effect(() => ctx.tuiTheme.register(kanagawaPalette, { activate: false }))
20
+ }
21
+
22
+ export { kanagawaPalette }
package/lib/mux.js CHANGED
@@ -21,6 +21,8 @@ const COMPOSITOR_METHODS = Object.freeze(new Set([
21
21
  CORDIS_METHODS.commandInvoke,
22
22
  CORDIS_METHODS.overlayEvent,
23
23
  CORDIS_METHODS.sessionConfigSet,
24
+ CORDIS_METHODS.approvalRespond,
25
+ CORDIS_METHODS.uiSelected,
24
26
  ]))
25
27
 
26
28
  /** Agent → TUI Node extras. Not compositor paint; Rust never sees these. */
@@ -0,0 +1,44 @@
1
+ {
2
+ "id": "ayu",
3
+ "label": "Ayu",
4
+ "dark": {
5
+ "bg": "#0b0e14",
6
+ "surface": "#0b0e14",
7
+ "panel": "#1e232b",
8
+ "fg": "#bfbdb6",
9
+ "fg_secondary": "#686868",
10
+ "fg_tertiary": "#686868",
11
+ "caption": "#686868",
12
+ "brand": "#53bdfa",
13
+ "brand_soft": "#cda1fa",
14
+ "bubble_bg": "#1b3a5b",
15
+ "bubble_fg": "#bfbdb6",
16
+ "border": "#686868",
17
+ "code_bg": "#0b0e14",
18
+ "ok": "#7fd962",
19
+ "warn": "#f9af4f",
20
+ "err": "#ea6c73",
21
+ "hint": "#90e1c6",
22
+ "chip_bg": "#1e232b"
23
+ },
24
+ "light": {
25
+ "bg": "#f8f9fa",
26
+ "surface": "#f8f9fa",
27
+ "panel": "#d1d1d1",
28
+ "fg": "#5c6166",
29
+ "fg_secondary": "#686868",
30
+ "fg_tertiary": "#686868",
31
+ "caption": "#686868",
32
+ "brand": "#3199e1",
33
+ "brand_soft": "#9e75c7",
34
+ "bubble_bg": "#d3e1f5",
35
+ "bubble_fg": "#5c6166",
36
+ "border": "#686868",
37
+ "code_bg": "#f8f9fa",
38
+ "ok": "#6cbf43",
39
+ "warn": "#eca944",
40
+ "err": "#ea6c6d",
41
+ "hint": "#46ba94",
42
+ "chip_bg": "#d1d1d1"
43
+ }
44
+ }