@coda-console/plugin-check 0.5.6 → 0.5.7

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.
@@ -125,6 +125,106 @@
125
125
  }
126
126
  ]
127
127
  },
128
+ {
129
+ "id": "composer",
130
+ "displayName": "输入框模式",
131
+ "description": "输入框的三种模式:交接到新会话、新建子会话,以及打 ! 执行命令。停用后输入框里没有这几种模式。",
132
+ "version": "1.0.0",
133
+ "category": "工作流程",
134
+ "source": {
135
+ "kind": "builtin"
136
+ },
137
+ "dir": "src/resources/plugins/composer",
138
+ "title": "输入框模式",
139
+ "properties": {},
140
+ "main": "main.mjs",
141
+ "workflows": [],
142
+ "capabilities": [],
143
+ "services": [],
144
+ "groups": [],
145
+ "commands": [],
146
+ "gitCredentials": [],
147
+ "endpoints": [],
148
+ "views": [],
149
+ "composerModes": [
150
+ {
151
+ "id": "handoff",
152
+ "title": "交接到新会话",
153
+ "icon": "arrow.right.square",
154
+ "hint": "新会话承接这里的上下文,共享当前工作目录及其中的文件;这个会话保留,仍可继续使用。",
155
+ "placeholder": "接下来要完成什么?",
156
+ "sendLabel": "交接",
157
+ "agent": "own"
158
+ },
159
+ {
160
+ "id": "subagent",
161
+ "title": "子会话",
162
+ "icon": "arrow.triangle.branch",
163
+ "hint": "承接父会话近期上下文,沿用父会话工作目录;只启动子会话,不向父 Agent 发送这段要求。",
164
+ "placeholder": "描述交给子会话的任务…",
165
+ "sendLabel": "启动子会话",
166
+ "agent": "own",
167
+ "parameters": {
168
+ "properties": {
169
+ "replyTo": {
170
+ "type": "string",
171
+ "enum": [
172
+ "parent",
173
+ "sender"
174
+ ],
175
+ "enumItemLabels": [
176
+ "完成后回报父会话",
177
+ "完成后回报你"
178
+ ],
179
+ "enumDescriptions": [
180
+ "结果交给父 Agent,由它接着处理",
181
+ "做完提醒你,不叫醒父 Agent"
182
+ ],
183
+ "title": "回报对象",
184
+ "default": "parent",
185
+ "order": 0
186
+ }
187
+ }
188
+ },
189
+ "schema": {
190
+ "type": "object",
191
+ "properties": {
192
+ "replyTo": {
193
+ "title": "回报对象",
194
+ "type": "string",
195
+ "enum": [
196
+ "parent",
197
+ "sender"
198
+ ],
199
+ "enumItemLabels": [
200
+ "完成后回报父会话",
201
+ "完成后回报你"
202
+ ],
203
+ "enumDescriptions": [
204
+ "结果交给父 Agent,由它接着处理",
205
+ "做完提醒你,不叫醒父 Agent"
206
+ ],
207
+ "default": "parent"
208
+ }
209
+ }
210
+ }
211
+ },
212
+ {
213
+ "id": "shell",
214
+ "title": "执行命令",
215
+ "icon": "terminal",
216
+ "hint": "在会话工作目录执行,命令和结果作为这条消息发给 Agent。",
217
+ "placeholder": "输入要执行的命令…",
218
+ "sendLabel": "执行",
219
+ "agent": "off",
220
+ "trigger": "!"
221
+ }
222
+ ],
223
+ "agents": [],
224
+ "dependencies": [],
225
+ "extensionPoints": {},
226
+ "extensions": []
227
+ },
128
228
  {
129
229
  "id": "github",
130
230
  "displayName": "GitHub",
@@ -442,7 +542,7 @@
442
542
  {
443
543
  "id": "session",
444
544
  "displayName": "会话生命周期",
445
- "description": "所有会话共用:存续期间管理空闲进程和任务转交,归档宽限后收尾,按设置回收托管工作树。停用后不自动执行这些政策。",
545
+ "description": "所有会话共用:存续期间管理空闲进程和任务转交,归档宽限后收尾,按设置回收托管工作树,限定同时执行的子会话。停用后不自动执行这些政策。",
446
546
  "version": "1.0.0",
447
547
  "category": "工作流程",
448
548
  "source": {
@@ -488,6 +588,15 @@
488
588
  "title": "Agent 停下多久后转给你",
489
589
  "order": 9,
490
590
  "default": "PT2M"
591
+ },
592
+ "session.subagents": {
593
+ "type": "integer",
594
+ "minimum": 1,
595
+ "maximum": 64,
596
+ "description": "Agent 开的子会话为父会话干活时各占一个名额;满了再开会当场失败,Agent 等其中一个做完再开,或者自己做。按这台机器算。",
597
+ "title": "同时执行的子会话",
598
+ "order": 10,
599
+ "default": 5
491
600
  }
492
601
  },
493
602
  "workflows": [
@@ -495,7 +604,12 @@
495
604
  ],
496
605
  "capabilities": [],
497
606
  "services": [],
498
- "groups": [],
607
+ "groups": [
608
+ {
609
+ "name": "subagents",
610
+ "capacity": "session.subagents"
611
+ }
612
+ ],
499
613
  "commands": [],
500
614
  "gitCredentials": [],
501
615
  "endpoints": [],
@@ -51,7 +51,7 @@ const PACKAGE_ROOT = fileURLToPath(new URL('../../../', import.meta.url));
51
51
  // What a plugin may contribute to Coda itself; anything else in `contributes` is for a point another plugin declares to
52
52
  // take it (the resources plugin's `panels`), refused when none does, as any other way of writing Coda cannot read.
53
53
  export const CONTRIBUTES = ['configuration', 'capabilities', 'instructions', 'agents', 'extensionPoints', 'extensions', 'groups', 'events', 'commands', 'gitCredentials',
54
- 'repositories', 'services', 'setup', 'skills', 'environment', 'path', 'endpoints', 'mcpServers', 'views', 'modelProviders', 'executors'];
54
+ 'repositories', 'services', 'setup', 'skills', 'environment', 'path', 'endpoints', 'mcpServers', 'views', 'modelProviders', 'executors', 'composerModes'];
55
55
  // The string formats a setting may have, beside `coda-agent` (an object) and `coda-agent-match` (a table's row names):
56
56
  // `coda-model-provider` names a model service another plugin provides (docs/plugins.md「模型服务」); `coda-model-url` is the
57
57
  // address of one, its Key and API in fields beside it (`modelKey`, `modelApi`), which Coda checks (「检查可用模型」).
@@ -81,19 +81,22 @@ const SYMBOL = /^[a-z0-9]+(\.[a-z0-9]+)*$/;
81
81
  // ---- Declarations ----
82
82
  // The JSON Schema Coda reads: a scalar, a choice of several among fixed options (an array of `items.anyOf` constants, as a
83
83
  // form's multi-select), several model services (an array of `items` of format `coda-model-provider`, which the
84
- // machine's model services are offered for), or an object whose entries all share one schema (VS Code shows such a setting as a table): a single
85
- // value, an Agent combination, or a row of named columns, each a single value, secrets among them (docs/plugins.md「类型」). Anything else is refused when the plugin is read, not ignored. `level`: what is read, a
86
- // setting, a table's entry, or a column of its rows.
84
+ // machine's model services are offered for), a list of rows (an array of `items` an object of named columns), or an object
85
+ // whose entries all share one schema (VS Code shows such a setting as a table): a single value, an Agent combination, or a
86
+ // row of named columns, each a single value or an Agent combination, secrets among them (docs/plugins.md「类型」). Anything
87
+ // else is refused when the plugin is read, not ignored. `level`: what is read, a setting, a table's entry, or a column of its rows.
87
88
  const COLUMN = /^[a-z][A-Za-z0-9]{0,39}$/, OPTION = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
89
+ // How many rows a list of them holds.
90
+ const ROWS_AT_MOST = 50;
88
91
  function schema(value, where, level = 'setting') {
89
92
  check(value && typeof value === 'object' && !Array.isArray(value), 'invalid_plugin', `${where} 须为 JSON Schema 对象`);
90
- const known = ['type', 'default', 'description', 'markdownDescription', 'title', 'enum', 'enumDescriptions', 'minimum', 'maximum', 'format', 'order',
93
+ const known = ['type', 'default', 'description', 'markdownDescription', 'title', 'enum', 'enumDescriptions', 'enumItemLabels', 'minimum', 'maximum', 'format', 'order',
91
94
  ...{ setting: ['additionalProperties', 'propertyNames', 'writeOnly', 'modelKey', 'modelApi', 'items', 'uniqueItems'], entry: ['properties', 'required'],
92
95
  column: ['writeOnly', 'modelKey', 'modelApi'] }[level]];
93
- check(level !== 'column' || value.type !== 'object', 'invalid_plugin', `${where}:一列只能是单个值`);
96
+ check(level !== 'column' || value.type !== 'object' || value.format === 'coda-agent', 'invalid_plugin', `${where}:一列只能是单个值或 Agent 组合`);
94
97
  check(level !== 'entry' || value.type !== 'object' || value.format === 'coda-agent' || value.properties !== undefined, 'invalid_plugin',
95
98
  `${where}:表格的每一项只能是单个值、Agent 组合,或 properties 写明的 1–20 列`);
96
- check(value.type !== 'array' || level === 'setting', 'invalid_plugin', `${where}:多选只能是插件的一项设置,不能放进表格`);
99
+ check(value.type !== 'array' || level === 'setting', 'invalid_plugin', `${where}:多选与一列行只能是插件的一项设置,不能放进表格`);
97
100
  const extra = Object.keys(value).filter(k => !known.includes(k));
98
101
  check(!extra.length, 'invalid_plugin', `${where} 里有 Coda 不支持的键:${extra.join('、')}`);
99
102
  check((value.properties === undefined && value.required === undefined) || (value.type === 'object' && value.format === undefined), 'invalid_plugin',
@@ -104,14 +107,20 @@ function schema(value, where, level = 'setting') {
104
107
  'invalid_plugin', `${where}:密文设置(writeOnly)只能是没有 format、enum 与默认值的文本`);
105
108
  const agent = value.format === 'coda-agent';
106
109
  check(['boolean', 'integer', 'number', 'string', 'object', 'array'].includes(value.type), 'invalid_plugin', `${where} 的 type 不受支持:${value.type}`);
107
- check((value.items === undefined && value.uniqueItems === undefined) || value.type === 'array', 'invalid_plugin', `${where}:只有多选(type: array)才写 items 与 uniqueItems`);
110
+ check((value.items === undefined && value.uniqueItems === undefined) || value.type === 'array', 'invalid_plugin', `${where}:只有多选与一列行(type: array)才写 items 与 uniqueItems`);
108
111
  check(value.format === undefined || agent || value.format === 'coda-agent-match' || STRING_FORMATS.includes(value.format), 'invalid_plugin', `${where} 的 format 不受支持:${value.format}`);
109
112
  check(!agent || value.type === 'object', 'invalid_plugin', `${where}:coda-agent 是对象`);
110
113
  check(!STRING_FORMATS.includes(value.format) || value.type === 'string', 'invalid_plugin', `${where}:${value.format} 是文本`);
111
114
  check(value.enum === undefined || (Array.isArray(value.enum) && value.enum.length && ['string', 'integer', 'number'].includes(value.type)), 'invalid_plugin', `${where} 的 enum 无效`);
115
+ // What each option is shown as, as VS Code's settings say it.
116
+ check(value.enumItemLabels === undefined || (Array.isArray(value.enum) && Array.isArray(value.enumItemLabels) && value.enumItemLabels.length === value.enum.length
117
+ && value.enumItemLabels.every(label => typeof label === 'string' && label.length <= 100)), 'invalid_plugin', `${where} 的 enumItemLabels 须与 enum 一一对应`);
112
118
  check((value.modelKey === undefined && value.modelApi === undefined) || value.format === 'coda-model-url', 'invalid_plugin',
113
119
  `${where}:modelKey 与 modelApi 只写在 coda-model-url 的地址上`);
114
120
  const out = { type: value.type, ...(value.format ? { format: value.format } : {}), ...(value.enum ? { enum: value.enum } : {}),
121
+ ...(value.enumItemLabels ? { enumItemLabels: value.enumItemLabels } : {}),
122
+ ...(Array.isArray(value.enumDescriptions) && value.enum && value.enumDescriptions.length === value.enum.length && value.enumDescriptions.every(d => typeof d === 'string')
123
+ ? { enumDescriptions: value.enumDescriptions } : {}),
115
124
  ...(value.minimum !== undefined ? { minimum: value.minimum } : {}), ...(value.maximum !== undefined ? { maximum: value.maximum } : {}),
116
125
  ...(typeof value.description === 'string' ? { description: value.description } : typeof value.markdownDescription === 'string' ? { description: value.markdownDescription } : {}),
117
126
  ...(typeof value.title === 'string' ? { title: value.title } : {}), ...(Number.isFinite(value.order) ? { order: value.order } : {}),
@@ -129,6 +138,14 @@ function schema(value, where, level = 'setting') {
129
138
  && items.modelApis.every(isModelApi)), 'invalid_plugin', `${where}.items.modelApis 须为 1–10 个接口名,取 ${MODEL_APIS.join('、')}`);
130
139
  out.format = 'coda-model-provider';
131
140
  if (items.modelApis) out.modelApis = [...new Set(items.modelApis)];
141
+ } else if (value.type === 'array' && value.items?.type === 'object') {
142
+ // A list of rows, each of named columns as a table's row is, kept in the order written: what has no name of its own.
143
+ check(value.format === undefined && value.minimum === undefined && value.maximum === undefined && value.uniqueItems === undefined, 'invalid_plugin',
144
+ `${where}:一列行不写 format、minimum、maximum 与 uniqueItems`);
145
+ check(value.items.format === undefined && value.items.properties !== undefined, 'invalid_plugin', `${where}:一列行写 items: { type: object, properties, required }`);
146
+ out.rows = schema(value.items, `${where}.items`, 'entry');
147
+ // Taken whole, as a value is: a secret is written only column by column, as a table's row has it.
148
+ check(!Object.values(out.rows.columns).some(column => column.writeOnly), 'invalid_plugin', `${where}:一列行的列不能是密文,要密文用表格`);
132
149
  } else if (value.type === 'array') {
133
150
  // A choice of several: each option a constant of its own, with what the client shows for it; chosen ones kept in this order.
134
151
  check(value.format === undefined && value.minimum === undefined && value.maximum === undefined, 'invalid_plugin', `${where}:多选不写 format、minimum 与 maximum`);
@@ -184,6 +201,64 @@ function modelFields(fields, where) {
184
201
  }
185
202
  }
186
203
 
204
+ // The modes a composer can be put in (docs/plugins.md「输入框模式」): what it shows then — an icon and a title, a line on
205
+ // what sending does, its placeholder and send button —, whom what is sent is for (`agent`: the session's agent, `own` one
206
+ // chosen for it, which a new session runs, `off` none shown), and its parameters, written as settings are. One entered by a
207
+ // character typed first (`trigger`, as `!`) is the text's: it has no parameters, and what is sent in it is the session's.
208
+ export const MODE_AGENTS = ['session', 'own', 'off'];
209
+ const MODE_KEYS = ['id', 'title', 'icon', 'hint', 'placeholder', 'sendLabel', 'agent', 'trigger', 'parameters'];
210
+ const oneLine = (text, most) => typeof text === 'string' && !!text.trim() && [...text].length <= most && !/[\0\r\n]/.test(text);
211
+ function composerModeDeclarations(value) {
212
+ const modes = value ?? [];
213
+ check(Array.isArray(modes) && modes.length <= 10, 'invalid_plugin', 'contributes.composerModes 须为数组,至多 10 个');
214
+ const out = modes.map((m, i) => {
215
+ const where = `contributes.composerModes[${i}]`;
216
+ check(m && typeof m === 'object' && !Array.isArray(m) && Object.keys(m).every(k => MODE_KEYS.includes(k)), 'invalid_plugin',
217
+ `${where} 只能写 ${MODE_KEYS.join('、')}`);
218
+ check(typeof m.id === 'string' && COMMAND.test(m.id), 'invalid_plugin', `${where}.id 是小写字母、数字和「-」,以字母开头,不超过 40 个字符`);
219
+ check(oneLine(m.title, 40), 'invalid_plugin', `${where}.title 是不超过 40 字的一行`);
220
+ check(typeof m.icon === 'string' && m.icon.length <= 100 && SYMBOL.test(m.icon), 'invalid_plugin', `${where}.icon 是 SF Symbol 的名字`);
221
+ check(m.hint === undefined || oneLine(m.hint, 200), 'invalid_plugin', `${where}.hint 是不超过 200 字的一行`);
222
+ check(m.placeholder === undefined || oneLine(m.placeholder, 40), 'invalid_plugin', `${where}.placeholder 是不超过 40 字的一行`);
223
+ check(m.sendLabel === undefined || oneLine(m.sendLabel, 20), 'invalid_plugin', `${where}.sendLabel 是不超过 20 字的一行`);
224
+ check(m.agent === undefined || MODE_AGENTS.includes(m.agent), 'invalid_plugin', `${where}.agent 只能是 ${MODE_AGENTS.join('、')}`);
225
+ check(m.trigger === undefined || (typeof m.trigger === 'string' && /^[!#$%&*+:;=?^|~]$/.test(m.trigger)), 'invalid_plugin',
226
+ `${where}.trigger 是一个标点:!#$%&*+:;=?^|~ 之一`);
227
+ check(m.trigger === undefined || (m.parameters === undefined && m.agent !== 'own'), 'invalid_plugin', `${where}:打字进入的模式(trigger)没有参数,也不另选 Agent`);
228
+ return { id: m.id, title: m.title.trim(), icon: m.icon, ...(m.hint ? { hint: m.hint.trim() } : {}), ...(m.placeholder ? { placeholder: m.placeholder.trim() } : {}),
229
+ ...(m.sendLabel ? { sendLabel: m.sendLabel.trim() } : {}), agent: m.agent ?? 'session', ...(m.trigger ? { trigger: m.trigger } : {}),
230
+ // As read, which the clients show; as written (JSON Schema), which its hooks are given (components/composer-modes).
231
+ ...(m.parameters !== undefined ? { parameters: modeParameters(m.parameters, `${where}.parameters`), schema: structuredClone(m.parameters) } : {}) };
232
+ });
233
+ check(new Set(out.map(m => m.id)).size === out.length, 'invalid_plugin', 'contributes.composerModes 的 id 不能重复');
234
+ const triggers = out.filter(m => m.trigger).map(m => m.trigger);
235
+ check(new Set(triggers).size === triggers.length, 'invalid_plugin', 'contributes.composerModes 的 trigger 不能重复');
236
+ return out;
237
+ }
238
+ // A mode's parameters: an object of named properties, each read as a setting is but a secret; what it cannot go without, `required`.
239
+ export function modeParameters(value, where) {
240
+ check(value && typeof value === 'object' && !Array.isArray(value) && value.type === 'object' && Object.keys(value).every(k => ['type', 'properties', 'required'].includes(k)),
241
+ 'invalid_plugin', `${where} 须为 { type: object, properties, required? }`);
242
+ const properties = value.properties;
243
+ check(properties && typeof properties === 'object' && !Array.isArray(properties) && Object.keys(properties).length >= 1 && Object.keys(properties).length <= 20,
244
+ 'invalid_plugin', `${where}.properties 须为 1–20 个参数`);
245
+ // In the order written, which the clients show them in: JSON's objects keep none of their own there.
246
+ const ordered = (fields, at) => Object.fromEntries(Object.entries(fields).map(([name, spec], i) => [name, { ...spec, order: spec.order ?? at + i }]));
247
+ const out = Object.fromEntries(Object.entries(properties).map(([name, p], i) => {
248
+ check(COLUMN.test(name), 'invalid_plugin', `${where} 的参数名只用字母和数字,以小写字母开头:${name}`);
249
+ const read = schema(p, `${where}.properties.${name}`);
250
+ const rows = read.rows ? { rows: { ...read.rows, columns: ordered(read.rows.columns, 0) } } : {};
251
+ const spec = { ...read, ...rows, order: read.order ?? i };
252
+ const secret = s => s.writeOnly || Object.values(s.columns ?? s.rows?.columns ?? s.entries?.columns ?? {}).some(c => c.writeOnly);
253
+ check(!secret(spec), 'invalid_plugin', `${where}.properties.${name}:参数不能是密文`);
254
+ return [name, spec];
255
+ }));
256
+ const required = value.required ?? [];
257
+ check(Array.isArray(required) && required.every(name => typeof name === 'string' && Object.hasOwn(out, name)) && new Set(required).size === required.length,
258
+ 'invalid_plugin', `${where}.required 须为本模式的参数名,不重复`);
259
+ return { properties: out, ...(required.length ? { required: [...required] } : {}) };
260
+ }
261
+
187
262
  // The category it is listed under (docs/plugins.md「商店」): the first of its manifest's `categories`, as VS Code's are
188
263
  // written; empty when it names none. Not checked: readPlugin does that.
189
264
  export const manifestCategory = manifest => Array.isArray(manifest?.categories) && typeof manifest.categories[0] === 'string' ? manifest.categories[0].trim().slice(0, 32) : '';
@@ -288,6 +363,7 @@ export function readPlugin(dir, source) {
288
363
  && (v.readout === undefined || VIEW_READOUTS.includes(v.readout)) && (v.checkout === undefined || VIEW_CHECKOUTS.includes(v.checkout)))
289
364
  && new Set(views.map(v => v.id)).size === views.length, 'invalid_plugin',
290
365
  `contributes.views 须为 { id, title, entry, in?, icon?, handles? } 的数组,至多 10 个:id 是小写字母、数字和「-」,以字母开头,不重复;title 是不超过 40 字的一行;entry 是插件目录里的 .html 文件;in 是它常驻的位置(${VIEW_PLACES.join('、')}),不重复;icon 是 SF Symbol 的名字;handles 是它接管打开的引用种类(如 workflow-runs),至多 8 个,不重复;readout 是它的读数(${VIEW_READOUTS.join('、')});checkout 是它只给哪种工作位置的会话(${VIEW_CHECKOUTS.join('、')})`);
366
+ const composerModes = composerModeDeclarations(manifest.contributes?.composerModes);
291
367
  const handled = views.flatMap(v => v.handles ?? []);
292
368
  check(new Set(handled).size === handled.length, 'invalid_plugin', 'contributes.views:同一种引用只能由一个视图接管');
293
369
  for (const v of views) {
@@ -337,6 +413,7 @@ export function readPlugin(dir, source) {
337
413
  views: views.map(v => ({ id: v.id, title: v.title, entry: posix.normalize(v.entry), ...(v.in?.length ? { in: [...v.in] } : {}), ...(v.icon ? { icon: v.icon } : {}),
338
414
  ...(v.handles?.length ? { handles: [...v.handles] } : {}), ...(v.readout ? { readout: v.readout } : {}),
339
415
  ...(v.checkout ? { checkout: v.checkout } : {}) })),
416
+ ...(composerModes.length ? { composerModes } : {}),
340
417
  // What it writes for another plugin's points, as written (components/plugins/plugin-extensions.mjs offerDeclarations).
341
418
  ...(Object.keys(offers).length ? { offers } : {}),
342
419
  agents: agentDeclarations(manifest.contributes?.agents, dir),
@@ -647,6 +724,16 @@ export function checkValue(spec, value, where, code = 'invalid_input') {
647
724
  }));
648
725
  }
649
726
  if (spec.type === 'boolean') { check(typeof value === 'boolean', code, `${where}须为开或关`); return value; }
727
+ // Rows in the order written, taken whole: a column not written is its default, as a table's row is resolved (rowValue),
728
+ // so that one a row cannot go without is there.
729
+ if (spec.type === 'array' && spec.rows) {
730
+ check(Array.isArray(value) && value.length <= ROWS_AT_MOST, code, `${where}须为至多 ${ROWS_AT_MOST} 行`);
731
+ const defaults = Object.entries(spec.rows.columns).filter(([, column]) => column.default !== undefined);
732
+ return value.map((row, i) => {
733
+ const checked = checkValue(spec.rows, row, `${where}第 ${i + 1} 行`, code);
734
+ return { ...Object.fromEntries(defaults.map(([name, column]) => [name, column.default])), ...checked };
735
+ });
736
+ }
650
737
  // Model services, each once, in the order chosen: one named may be of a plugin not installed (yet), as a single one may.
651
738
  if (spec.type === 'array' && spec.format === 'coda-model-provider') {
652
739
  check(Array.isArray(value) && value.length <= MODEL_PROVIDERS_AT_MOST && value.every(v => typeof v === 'string' && MODEL_PROVIDER.test(v)), code,
@@ -761,7 +848,7 @@ function read(item, spec, from) {
761
848
  // Of options chosen once, those the plugin has no more are not chosen; a value stored before the setting was a choice is none.
762
849
  if (spec.type === 'array' && spec.format === 'coda-model-provider')
763
850
  return Array.isArray(item.value) ? { value: item.value.filter(v => typeof v === 'string' && MODEL_PROVIDER.test(v)), from } : null;
764
- if (spec.type === 'array') return Array.isArray(item.value) ? { value: spec.options.map(o => o.value).filter(v => item.value.includes(v)), from } : null;
851
+ if (spec.type === 'array' && spec.options) return Array.isArray(item.value) ? { value: spec.options.map(o => o.value).filter(v => item.value.includes(v)), from } : null;
765
852
  return { value: item.value, from };
766
853
  }
767
854
  // Every setting of a plugin for one project (or for every project, without one): project over plugin over default.
@@ -254,11 +254,13 @@ export function builtinProblem(id, source, builtin, installed) {
254
254
  return `与内置插件 ${id} 同名${copy.source?.repo || copy.source?.package ? ',来源不同' : ''},请先卸载内置插件再安装`;
255
255
  }
256
256
  // What only one plugin may declare, as said to the user: an agent (sessions name it by its ID alone), a concurrency group
257
- // (any process takes it by its name), Git's credentials for an address, opening a kind of reference (a view's `handles`)
257
+ // (any process takes it by its name), Git's credentials for an address, opening a kind of reference (a view's `handles`),
258
+ // a composer mode's name and the character typed to enter one (the composer finds it by them)
258
259
  // and a key of `contributes` a point takes. A command, as a Skill or a variable, is a
259
260
  // project's: one two plugins of a project bring is neither's there (components/plugins/plugins.mjs launch).
260
261
  export const claims = plugin => [...plugin.agents.map(a => `Agent ${a.id}`), ...plugin.groups.map(g => `组 ${g.name}`),
261
262
  ...(plugin.gitCredentials ?? []).map(c => `${c.url} 的 Git 凭据`), ...(plugin.views ?? []).flatMap(v => (v.handles ?? []).map(kind => `打开 ${kind} 引用`)),
263
+ ...(plugin.composerModes ?? []).flatMap(m => [`输入框模式 /${m.id}`, ...(m.trigger ? [`输入框里打 ${m.trigger} 进入的模式`] : [])]),
262
264
  ...Object.values(plugin.extensionPoints ?? {}).filter(p => p.contributes).map(p => `扩展点 contributes.${p.contributes}`)];
263
265
  // Why a plugin cannot sit beside those this machine has (`catalog`: built-in and installed, by ID; `builtin`: the plugins
264
266
  // Coda ships, by ID), or null: an installed built-in one's ID from another source (builtinProblem), or an agent, a group
@@ -328,6 +330,8 @@ export function contributions(plugin, catalog = new Map()) {
328
330
  ...(plugin.views?.length ? { views: plugin.views.map(v => ({ id: v.id, title: v.title, ...(v.in ? { in: v.in } : {}), ...(v.icon ? { icon: v.icon } : {}), ...(v.handles ? { handles: v.handles } : {}), ...(v.readout ? { readout: v.readout } : {}),
329
331
  ...(v.checkout ? { checkout: v.checkout } : {}) })) } : {}),
330
332
  groups: plugin.groups.map(g => ({ name: g.name, capacity: g.capacity })),
333
+ // The modes it puts the composer in, as declared: the clients show them from this.
334
+ ...(plugin.composerModes?.length ? { composerModes: plugin.composerModes.map(({ schema: _, ...mode }) => mode) } : {}),
331
335
  extensions: plugin.extensions.map(e => ({ plugin: e.plugin, point: e.point, command: e.command })),
332
336
  // What it writes for points under keys of `contributes` (the resources plugin's panels): by point, how many and the
333
337
  // commands in them, each with what it is for.
@@ -42,11 +42,20 @@ export function quotes(value = []) {
42
42
  return passages;
43
43
  }
44
44
 
45
+ // The composer mode a message was sent in (docs/plugins.md「输入框模式」), as it was then: `<plugin>/<mode>`, its title and
46
+ // icon, which its bubble shows however the plugin changes later.
47
+ export function modeMark(value) {
48
+ object(value, '输入框模式');
49
+ keys(value, ['id', 'title', 'icon']);
50
+ check(typeof value.id === 'string' && /^[a-z][a-z0-9-]{0,39}\/[a-z][a-z0-9-]{0,39}$/.test(value.id), 'invalid_input', '输入框模式须写成「插件/模式」');
51
+ return { id: value.id, title: text(value.title, '模式名', 40), icon: text(value.icon, '模式图标', 100) };
52
+ }
53
+
45
54
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
46
55
  export function firstMessage(value) {
47
56
  object(value, '首次发送');
48
57
  keys(value, ['requestId', 'projectId', 'title', 'agent', 'checkout', 'settings', 'text', 'attachments', 'quotes', 'handoffSource', 'parentThreadId',
49
- 'replyTo', 'threadId', 'extensionParent']);
58
+ 'replyTo', 'threadId', 'extensionParent', 'mode']);
50
59
  const handoff = value.handoffSource === undefined ? {} : { handoffSource: text(value.handoffSource, '交接来源', 100) };
51
60
  const parent = value.parentThreadId === undefined ? {} : { parentThreadId: text(value.parentThreadId, '父会话', 100) };
52
61
  check(!parent.parentThreadId || handoff.handoffSource === parent.parentThreadId, 'invalid_input', '子会话必须引用父会话上下文');
@@ -96,6 +105,7 @@ export function firstMessage(value) {
96
105
  settings: settings(value.settings),
97
106
  text: message,
98
107
  ...(passages.length ? { quotes: passages } : {}),
108
+ ...(value.mode === undefined ? {} : { mode: modeMark(value.mode) }),
99
109
  }, attachments(value.attachments));
100
110
  }
101
111
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "commit": "93e1f9059276267bbf0aa4cd2979a98e199b6547",
2
+ "commit": "b705ff8840dadc567d2515df4a295d3448e0f995",
3
3
  "packages": {
4
4
  "@0dep/piso": "5.2.0",
5
5
  "@agentclientprotocol/sdk": "1.4.0",
@@ -9,11 +9,11 @@
9
9
  "check/coda/components/plugins/agent-plugins.mjs": "23016dd0f7350a10f71565a2c44e2c1179299a93e8c859529071099f2ce862d1",
10
10
  "check/coda/components/plugins/model-protocols.mjs": "762244c01266cf61384377bdfbb84bef5ab157b1783f9254ddc1ae3d576a73c5",
11
11
  "check/coda/components/plugins/plugin-extensions.mjs": "e527800a109ec9849975357f841b0c05ecfa50c9d9c34e26d828aef3cb7d3cb3",
12
- "check/coda/components/plugins/plugin-rules.mjs": "78d9431c1b4f6354457f153bb74a0fd085c771eaadd9f7523633915cb700f4f9",
13
- "check/coda/components/plugins/plugin-source-rules.mjs": "d620ee62fb372e31e5389e27f7202308aab74324b6f5552ffef3844afc7d0001",
12
+ "check/coda/components/plugins/plugin-rules.mjs": "543fdd8708e9c9a4508e42173bab68a8d7073cd4cc04d8a704d9a42fd82cb5b0",
13
+ "check/coda/components/plugins/plugin-source-rules.mjs": "e03606bf3056061ee3a78494d34dae9dfbe35d6fb2f139880fab5891663cbedc",
14
14
  "check/coda/components/plugins/skill-document.mjs": "c5b5e815c42cd836572f7b60e22a46af6ac607fd9df0170fa36f955825636ce6",
15
15
  "check/coda/components/prompt-recovery/prompt-recovery.mjs": "f43f21d24c19a58113d3304f9248225b12080ab00ed15eab74bf7b94642732de",
16
- "check/coda/components/session-config/commands.mjs": "96247da877e455f835e0ee41622e531936e2600e911c2e9a4638fda00e987ec3",
16
+ "check/coda/components/session-config/commands.mjs": "2ccc0c089d88b3f76d9098876dc60bd7e06348931773da131732c0d6c6c7a6cf",
17
17
  "check/coda/components/workflows/executor-labels.mjs": "dceca6ae60c0a4f3c3cc72a6c84f1489831790774c93caab96e4ddb493bc89e1",
18
18
  "check/coda/domain/agent-protocol.mjs": "7337cf0af97a9592bbcad98a0e4f3837d8bb4d48b53c22846a8c56a0c88a4630",
19
19
  "check/coda/domain/duration.mjs": "15dd64ef9b02342ad521c66e5968a569432a89dc7568d11afd3e6f3fe37e104b",
@@ -24,7 +24,7 @@
24
24
  "check/coda/resources/plugins/resources/panels/panels.mjs": "aee040febb37fd6eebaf1a1da5b3299d2b6a9dfc982e649d9a628291af298179",
25
25
  "check/coda/resources/plugins/resources/panels/rules.mjs": "c024c5ede7c1516877128aa6d6796fbe5fd0e14642741b0745481bb1c5ca41d3",
26
26
  "check/coda/resources/plugins/resources/panels/sign-in.mjs": "1271a24e33b1b882a1fb9bee03b8e240db8b9bee1b5208f0e1c84499cfb1be7e",
27
- "check/coda/builtin.json": "0e04c5a9bb82db5573d865791df6fe9e52295be470b1bcdf0add719d2d2c0b85",
27
+ "check/coda/builtin.json": "aa0787c6eb810490dcdec8874c3f3b2777020fd854f10225b9f3eadf348a7f52",
28
28
  "docs/agent-protocol.md": "2a40fac029e93bc512d0405105c9d74fa3e5a59e92788b6951dba182775875c1"
29
29
  }
30
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coda-console/plugin-check",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "Coda 插件的契约检查:扮演 Coda 读清单、启动 Agent 讲 ACP 与 _coda/wait、读面板。",
5
5
  "license": "MIT",
6
6
  "repository": {