@woosau/dsh-custom-env 0.1.8 → 0.1.9

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/client.js CHANGED
@@ -179,18 +179,27 @@ var ResultValue = class {
179
179
  };
180
180
 
181
181
  // lib/typert.remote-client.js
182
+ var envGroupSchema = () => import_zod.z.object({
183
+ id: import_zod.z.string().readonly(),
184
+ title: import_zod.z.string().readonly()
185
+ }).readonly();
186
+ var envEntrySchema = () => import_zod.z.object({
187
+ id: import_zod.z.string().readonly(),
188
+ name: import_zod.z.string().readonly(),
189
+ value: import_zod.z.string().readonly(),
190
+ groupId: import_zod.z.string().readonly(),
191
+ kind: import_zod.z.enum(["unchanged", "modified", "added"]).readonly(),
192
+ previous: import_zod.z.string().nullable().readonly()
193
+ }).readonly();
182
194
  var list$result$schema$value;
183
195
  var list$result$schema = () => list$result$schema$value ??= import_zod.z.object({
184
196
  path: import_zod.z.string().readonly(),
185
197
  exists: import_zod.z.boolean().readonly(),
186
- entries: import_zod.z.array(import_zod.z.object({
187
- id: import_zod.z.string().readonly(),
188
- name: import_zod.z.string().readonly(),
189
- value: import_zod.z.string().readonly(),
190
- kind: import_zod.z.enum(["unchanged", "modified", "added"]).readonly(),
191
- previous: import_zod.z.string().nullable().readonly()
192
- }).readonly()).readonly()
198
+ groups: import_zod.z.array(envGroupSchema()).readonly(),
199
+ entries: import_zod.z.array(envEntrySchema()).readonly()
193
200
  });
201
+ var envGroup$result$schema$value;
202
+ var envGroup$result$schema = () => envGroup$result$schema$value ??= envGroupSchema();
194
203
  var cliStatus$result$schema$value;
195
204
  var cliStatus$result$schema = () => cliStatus$result$schema$value ??= import_zod.z.object({
196
205
  platform: import_zod.z.string().readonly(),
@@ -201,38 +210,38 @@ var cliStatus$result$schema = () => cliStatus$result$schema$value ??= import_zod
201
210
  canInstall: import_zod.z.boolean().readonly(),
202
211
  reason: import_zod.z.string().nullable().readonly()
203
212
  });
204
- var upsert$parameter$0$schema$value;
205
- var upsert$parameter$0$schema = () => upsert$parameter$0$schema$value ??= import_zod.z.string();
206
- var upsert$parameter$1$schema$value;
207
- var upsert$parameter$1$schema = () => upsert$parameter$1$schema$value ??= import_zod.z.string();
208
- var upsert$parameter$2$schema$value;
209
- var upsert$parameter$2$schema = () => upsert$parameter$2$schema$value ??= import_zod.z.string();
210
- var remove$parameter$0$schema$value;
211
- var remove$parameter$0$schema = () => remove$parameter$0$schema$value ??= import_zod.z.string();
212
- var setVar$parameter$0$schema$value;
213
- var setVar$parameter$0$schema = () => setVar$parameter$0$schema$value ??= import_zod.z.string();
214
- var setVar$parameter$1$schema$value;
215
- var setVar$parameter$1$schema = () => setVar$parameter$1$schema$value ??= import_zod.z.string();
213
+ var str = () => import_zod.z.string();
216
214
  var setVars$parameter$0$schema$value;
217
215
  var setVars$parameter$0$schema = () => setVars$parameter$0$schema$value ??= import_zod.z.array(import_zod.z.object({
218
216
  name: import_zod.z.string().readonly(),
219
- value: import_zod.z.string().readonly()
217
+ value: import_zod.z.string().readonly(),
218
+ group: import_zod.z.string().optional()
220
219
  }).readonly()).readonly();
221
- var removeVar$parameter$0$schema$value;
222
- var removeVar$parameter$0$schema = () => removeVar$parameter$0$schema$value ??= import_zod.z.string();
223
220
  var removeVars$parameter$0$schema$value;
224
221
  var removeVars$parameter$0$schema = () => removeVars$parameter$0$schema$value ??= import_zod.z.array(import_zod.z.string()).readonly();
222
+ var deleteGroup$parameter$1$schema$value;
223
+ var deleteGroup$parameter$1$schema = () => deleteGroup$parameter$1$schema$value ??= import_zod.z.object({
224
+ moveToDefault: import_zod.z.boolean().readonly()
225
+ }).readonly();
225
226
  var envVarWriteJsonSchema = {
226
227
  type: "object",
227
228
  properties: {
228
229
  name: { type: "string" },
229
- value: { type: "string" }
230
+ value: { type: "string" },
231
+ group: { type: "string" }
230
232
  },
231
233
  required: ["name", "value"],
232
234
  additionalProperties: false
233
235
  };
234
- var restore$parameter$0$schema$value;
235
- var restore$parameter$0$schema = () => restore$parameter$0$schema$value ??= import_zod.z.string();
236
+ var envGroupJsonSchema = {
237
+ type: "object",
238
+ properties: {
239
+ id: { type: "string" },
240
+ title: { type: "string" }
241
+ },
242
+ required: ["id", "title"],
243
+ additionalProperties: true
244
+ };
236
245
  var cliStatusJsonSchema = {
237
246
  type: "object",
238
247
  properties: {
@@ -252,6 +261,10 @@ var envSnapshotJsonSchema = {
252
261
  properties: {
253
262
  path: { type: "string" },
254
263
  exists: { type: "boolean" },
264
+ groups: {
265
+ type: "array",
266
+ items: envGroupJsonSchema
267
+ },
255
268
  entries: {
256
269
  type: "array",
257
270
  items: {
@@ -260,90 +273,84 @@ var envSnapshotJsonSchema = {
260
273
  id: { type: "string" },
261
274
  name: { type: "string" },
262
275
  value: { type: "string" },
276
+ groupId: { type: "string" },
263
277
  kind: { type: "string", enum: ["unchanged", "modified", "added"] },
264
278
  previous: { type: ["string", "null"] }
265
279
  },
266
- required: ["id", "name", "value", "kind", "previous"],
280
+ required: ["id", "name", "value", "groupId", "kind", "previous"],
267
281
  additionalProperties: true
268
282
  }
269
283
  }
270
284
  },
271
- required: ["path", "exists", "entries"],
285
+ required: ["path", "exists", "groups", "entries"],
272
286
  additionalProperties: true
273
287
  };
288
+ var stringParam = (name, description, options = {}) => ({
289
+ name,
290
+ description,
291
+ source: "json",
292
+ ...options.acceptsUndefined ? { acceptsUndefined: true } : {},
293
+ codec: {
294
+ mode: "strict",
295
+ typeSymbol: "string",
296
+ create: str,
297
+ jsonSchema: { type: "string" }
298
+ }
299
+ });
300
+ var snapshotResult = (description = "updated snapshot.") => ({
301
+ mode: "strict",
302
+ typeSymbol: "@woosau/dsh-custom-env/types#EnvSnapshot",
303
+ create: list$result$schema,
304
+ description,
305
+ jsonSchema: envSnapshotJsonSchema
306
+ });
307
+ var groupResult = (description = "group record.") => ({
308
+ mode: "strict",
309
+ typeSymbol: "@woosau/dsh-custom-env/types#EnvGroup",
310
+ create: envGroup$result$schema,
311
+ description,
312
+ jsonSchema: envGroupJsonSchema
313
+ });
314
+ var voidResult = {
315
+ mode: "strict",
316
+ typeSymbol: "void",
317
+ create: () => import_zod.z.undefined(),
318
+ description: "no return value.",
319
+ jsonSchema: { type: "null" }
320
+ };
321
+ var src = { file: "src/index.ts", line: 1, column: 1 };
274
322
  var TYPERT_REMOTE = createTypertRemoteInfo({
275
323
  package: "@woosau/dsh-custom-env",
276
324
  methods: {
277
325
  "customEnv/list": {
278
326
  service: "customEnv",
279
- description: "List current $DSH_HOME/.env entries with pending classification.",
327
+ description: "List current $DSH_HOME/.env entries with groups and pending classification.",
280
328
  invocation: { kind: "direct" },
281
329
  parameters: [],
282
- result: {
283
- mode: "strict",
284
- typeSymbol: "@woosau/dsh-custom-env/types#EnvSnapshot",
285
- create: list$result$schema,
286
- description: "path, existence, and sorted entries.",
287
- jsonSchema: envSnapshotJsonSchema
288
- },
289
- sourceLocation: { file: "src/index.ts", line: 1, column: 1 }
330
+ result: snapshotResult("path, existence, groups, and sorted entries."),
331
+ sourceLocation: src
290
332
  },
291
333
  "customEnv/upsert": {
292
334
  service: "customEnv",
293
- description: "Create or update one row. Empty id creates a new row; otherwise keeps that baseline id across renames.",
335
+ description: "Create or update one row. Empty id creates a new row; optional groupId assigns membership.",
294
336
  invocation: { kind: "direct" },
295
337
  parameters: [
296
- {
297
- name: "id",
298
- description: "stable row id from list(), or empty string for a new row.",
299
- source: "json",
300
- codec: {
301
- mode: "strict",
302
- typeSymbol: "string",
303
- create: upsert$parameter$0$schema,
304
- jsonSchema: { type: "string" }
305
- }
306
- },
307
- {
308
- name: "name",
309
- description: "writable variable name.",
310
- source: "json",
311
- codec: {
312
- mode: "strict",
313
- typeSymbol: "string",
314
- create: upsert$parameter$1$schema,
315
- jsonSchema: { type: "string" }
316
- }
317
- },
318
- {
319
- name: "value",
320
- description: "value (empty string allowed).",
321
- source: "json",
322
- codec: {
323
- mode: "strict",
324
- typeSymbol: "string",
325
- create: upsert$parameter$2$schema,
326
- jsonSchema: { type: "string" }
327
- }
328
- }
338
+ stringParam("id", "stable row id from list(), or empty string for a new row."),
339
+ stringParam("name", "writable variable name."),
340
+ stringParam("value", "value (empty string allowed)."),
341
+ stringParam("groupId", "target group id for create/move (optional).", { acceptsUndefined: true })
329
342
  ],
330
- result: {
331
- mode: "strict",
332
- typeSymbol: "@woosau/dsh-custom-env/types#EnvSnapshot",
333
- create: list$result$schema,
334
- description: "updated snapshot.",
335
- jsonSchema: envSnapshotJsonSchema
336
- },
337
- sourceLocation: { file: "src/index.ts", line: 1, column: 1 }
343
+ result: snapshotResult(),
344
+ sourceLocation: src
338
345
  },
339
346
  "customEnv/setVars": {
340
347
  service: "customEnv",
341
- description: "Create or replace many variables by name in one rewrite. Empty values write NAME=.",
348
+ description: "Create or replace many variables by name in one rewrite. Optional group per entry.",
342
349
  invocation: { kind: "direct" },
343
350
  parameters: [
344
351
  {
345
352
  name: "entries",
346
- description: "name/value pairs (last duplicate name wins).",
353
+ description: "name/value pairs with optional group (last duplicate name wins).",
347
354
  source: "json",
348
355
  codec: {
349
356
  mode: "strict",
@@ -353,51 +360,20 @@ var TYPERT_REMOTE = createTypertRemoteInfo({
353
360
  }
354
361
  }
355
362
  ],
356
- result: {
357
- mode: "strict",
358
- typeSymbol: "void",
359
- create: () => import_zod.z.undefined(),
360
- description: "no return value.",
361
- jsonSchema: { type: "null" }
362
- },
363
- sourceLocation: { file: "src/index.ts", line: 1, column: 1 }
363
+ result: voidResult,
364
+ sourceLocation: src
364
365
  },
365
366
  "customEnv/setVar": {
366
367
  service: "customEnv",
367
- description: "Create or replace one variable by name. Empty value writes NAME=.",
368
+ description: "Create or replace one variable by name. Optional group: new names default to default; existing keep group when omitted.",
368
369
  invocation: { kind: "direct" },
369
370
  parameters: [
370
- {
371
- name: "name",
372
- description: "writable variable name.",
373
- source: "json",
374
- codec: {
375
- mode: "strict",
376
- typeSymbol: "string",
377
- create: setVar$parameter$0$schema,
378
- jsonSchema: { type: "string" }
379
- }
380
- },
381
- {
382
- name: "value",
383
- description: "value to store (empty string allowed).",
384
- source: "json",
385
- codec: {
386
- mode: "strict",
387
- typeSymbol: "string",
388
- create: setVar$parameter$1$schema,
389
- jsonSchema: { type: "string" }
390
- }
391
- }
371
+ stringParam("name", "writable variable name."),
372
+ stringParam("value", "value to store (empty string allowed)."),
373
+ stringParam("group", "optional group id; omit to keep existing membership.", { acceptsUndefined: true })
392
374
  ],
393
- result: {
394
- mode: "strict",
395
- typeSymbol: "void",
396
- create: () => import_zod.z.undefined(),
397
- description: "no return value.",
398
- jsonSchema: { type: "null" }
399
- },
400
- sourceLocation: { file: "src/index.ts", line: 1, column: 1 }
375
+ result: voidResult,
376
+ sourceLocation: src
401
377
  },
402
378
  "customEnv/removeVars": {
403
379
  service: "customEnv",
@@ -423,24 +399,14 @@ var TYPERT_REMOTE = createTypertRemoteInfo({
423
399
  description: "number of .env lines removed.",
424
400
  jsonSchema: { type: "number" }
425
401
  },
426
- sourceLocation: { file: "src/index.ts", line: 1, column: 1 }
402
+ sourceLocation: src
427
403
  },
428
404
  "customEnv/removeVar": {
429
405
  service: "customEnv",
430
406
  description: "Remove one variable by name from the file, Host, and OS user environment.",
431
407
  invocation: { kind: "direct" },
432
408
  parameters: [
433
- {
434
- name: "name",
435
- description: "writable variable name.",
436
- source: "json",
437
- codec: {
438
- mode: "strict",
439
- typeSymbol: "string",
440
- create: removeVar$parameter$0$schema,
441
- jsonSchema: { type: "string" }
442
- }
443
- }
409
+ stringParam("name", "writable variable name.")
444
410
  ],
445
411
  result: {
446
412
  mode: "strict",
@@ -449,59 +415,85 @@ var TYPERT_REMOTE = createTypertRemoteInfo({
449
415
  description: "true when a .env line was removed.",
450
416
  jsonSchema: { type: "boolean" }
451
417
  },
452
- sourceLocation: { file: "src/index.ts", line: 1, column: 1 }
418
+ sourceLocation: src
453
419
  },
454
420
  "customEnv/deleteEntry": {
455
421
  service: "customEnv",
456
422
  description: "Delete one entry from the file, Host, and OS user environment.",
457
423
  invocation: { kind: "direct" },
458
424
  parameters: [
459
- {
460
- name: "id",
461
- description: "stable row id from list().",
462
- source: "json",
463
- codec: {
464
- mode: "strict",
465
- typeSymbol: "string",
466
- create: remove$parameter$0$schema,
467
- jsonSchema: { type: "string" }
468
- }
469
- }
425
+ stringParam("id", "stable row id from list().")
470
426
  ],
471
- result: {
472
- mode: "strict",
473
- typeSymbol: "@woosau/dsh-custom-env/types#EnvSnapshot",
474
- create: list$result$schema,
475
- description: "updated snapshot.",
476
- jsonSchema: envSnapshotJsonSchema
477
- },
478
- sourceLocation: { file: "src/index.ts", line: 1, column: 1 }
427
+ result: snapshotResult(),
428
+ sourceLocation: src
479
429
  },
480
430
  "customEnv/restore": {
481
431
  service: "customEnv",
482
432
  description: "Restore Host-start original name+value for this id, or delete if added after start.",
483
433
  invocation: { kind: "direct" },
484
434
  parameters: [
435
+ stringParam("id", "stable row id from list().")
436
+ ],
437
+ result: snapshotResult(),
438
+ sourceLocation: src
439
+ },
440
+ "customEnv/createGroup": {
441
+ service: "customEnv",
442
+ description: "Create a custom environment variable group.",
443
+ invocation: { kind: "direct" },
444
+ parameters: [
445
+ stringParam("title", "non-empty display title.")
446
+ ],
447
+ result: groupResult("created group."),
448
+ sourceLocation: src
449
+ },
450
+ "customEnv/renameGroup": {
451
+ service: "customEnv",
452
+ description: "Rename a custom group (not the default group).",
453
+ invocation: { kind: "direct" },
454
+ parameters: [
455
+ stringParam("id", "group id."),
456
+ stringParam("title", "new non-empty title.")
457
+ ],
458
+ result: groupResult("renamed group."),
459
+ sourceLocation: src
460
+ },
461
+ "customEnv/deleteGroup": {
462
+ service: "customEnv",
463
+ description: "Delete a custom group; optionally move members to default or remove their variables.",
464
+ invocation: { kind: "direct" },
465
+ parameters: [
466
+ stringParam("id", "group id (not default)."),
485
467
  {
486
- name: "id",
487
- description: "stable row id from list().",
468
+ name: "options",
469
+ description: "moveToDefault controls whether members are kept.",
488
470
  source: "json",
489
471
  codec: {
490
472
  mode: "strict",
491
- typeSymbol: "string",
492
- create: restore$parameter$0$schema,
493
- jsonSchema: { type: "string" }
473
+ typeSymbol: "DeleteGroupOptions",
474
+ create: deleteGroup$parameter$1$schema,
475
+ jsonSchema: {
476
+ type: "object",
477
+ properties: { moveToDefault: { type: "boolean" } },
478
+ required: ["moveToDefault"],
479
+ additionalProperties: false
480
+ }
494
481
  }
495
482
  }
496
483
  ],
497
- result: {
498
- mode: "strict",
499
- typeSymbol: "@woosau/dsh-custom-env/types#EnvSnapshot",
500
- create: list$result$schema,
501
- description: "updated snapshot.",
502
- jsonSchema: envSnapshotJsonSchema
503
- },
504
- sourceLocation: { file: "src/index.ts", line: 1, column: 1 }
484
+ result: snapshotResult(),
485
+ sourceLocation: src
486
+ },
487
+ "customEnv/moveEntry": {
488
+ service: "customEnv",
489
+ description: "Move one row into another group.",
490
+ invocation: { kind: "direct" },
491
+ parameters: [
492
+ stringParam("id", "stable row id from list()."),
493
+ stringParam("groupId", "target group id.")
494
+ ],
495
+ result: snapshotResult(),
496
+ sourceLocation: src
505
497
  },
506
498
  "customEnv/cliStatus": {
507
499
  service: "customEnv",
@@ -515,7 +507,7 @@ var TYPERT_REMOTE = createTypertRemoteInfo({
515
507
  description: "active path, expected path, and whether Install should be offered.",
516
508
  jsonSchema: cliStatusJsonSchema
517
509
  },
518
- sourceLocation: { file: "src/index.ts", line: 1, column: 1 }
510
+ sourceLocation: src
519
511
  },
520
512
  "customEnv/cliInstall": {
521
513
  service: "customEnv",
@@ -529,7 +521,7 @@ var TYPERT_REMOTE = createTypertRemoteInfo({
529
521
  description: "refreshed status after install.",
530
522
  jsonSchema: cliStatusJsonSchema
531
523
  },
532
- sourceLocation: { file: "src/index.ts", line: 1, column: 1 }
524
+ sourceLocation: src
533
525
  }
534
526
  }
535
527
  }, import_zod.z);
@@ -539,6 +531,9 @@ var typert_remote_client_default = TYPERT_REMOTE;
539
531
  var import_react = require("react");
540
532
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
541
533
 
534
+ // src/group-ids.ts
535
+ var DEFAULT_GROUP_ID = "default";
536
+
542
537
  // src/client/format-remote-error.ts
543
538
  function isRemoteFailure(value) {
544
539
  return typeof value === "object" && value !== null && value.isDSHRemoteError === true && typeof value.code === "string";
@@ -547,6 +542,7 @@ function formatRemoteError(error, t) {
547
542
  if (isRemoteFailure(error)) {
548
543
  if (error.code === "custom-env/invalid-name") return t("error.invalidName");
549
544
  if (error.code === "custom-env/invalid-value") return t("error.invalidValue");
545
+ if (error.code === "custom-env/invalid-group") return t("error.invalidGroup");
550
546
  if (error.code === "custom-env/restore-failed") return t("error.restoreFailed");
551
547
  if (error.code === "custom-env/cli-install-failed") return t("error.cliInstall");
552
548
  }
@@ -626,23 +622,41 @@ function rowIdFromPoint(clientX, clientY) {
626
622
  }
627
623
 
628
624
  // src/client/EnvSettingsSection.module.css
629
- var css = ".EnvSettingsSection_section {\n display: flex;\n flex-direction: column;\n gap: 16px;\n max-width: 760px;\n color: var(--dsw-alias-label-primary);\n}\n\n.EnvSettingsSection_heading {\n margin: 0;\n font-size: 18px;\n font-weight: 500;\n line-height: 1.3;\n}\n\n/* \u2014\u2014 dsh CLI: exactly two lines (label + path) \u2014\u2014 */\n.EnvSettingsSection_cli {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.EnvSettingsSection_cliHeading {\n margin: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.EnvSettingsSection_cliBody {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 28px;\n gap: 8px;\n align-items: center;\n height: 28px;\n}\n\n.EnvSettingsSection_cliPath {\n margin: 0;\n min-width: 0;\n font-size: 13px;\n line-height: 28px;\n color: var(--dsw-alias-label-tertiary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.EnvSettingsSection_cliSlot {\n display: flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n}\n\n.EnvSettingsSection_cliSlot[data-empty='true'] {\n visibility: hidden;\n pointer-events: none;\n}\n\n.EnvSettingsSection_notice,\n.EnvSettingsSection_empty {\n margin: 0;\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.EnvSettingsSection_notice[data-tone='error'] {\n color: var(--dsw-alias-label-danger, #c62828);\n}\n\n/* \u2014\u2014 variable list: CSS grid, not table (avoids Input overflow overlap) \u2014\u2014 */\n.EnvSettingsSection_list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 0;\n}\n\n.EnvSettingsSection_header,\n.EnvSettingsSection_row {\n display: grid;\n grid-template-columns: minmax(120px, 0.85fr) minmax(0, 1.55fr) 100px;\n gap: 10px;\n align-items: center;\n min-width: 0;\n}\n\n.EnvSettingsSection_row {\n padding: 4px 6px;\n margin: 0 -6px;\n user-select: none;\n}\n\n.EnvSettingsSection_row[data-selected='true'] {\n background: var(--dsw-alias-fill-hover, rgba(127, 127, 127, 0.1));\n}\n\n.EnvSettingsSection_row[data-composer='true'] {\n user-select: auto;\n}\n\n.EnvSettingsSection_header {\n padding: 0 0 2px;\n}\n\n.EnvSettingsSection_headerLabel {\n margin: 0;\n font-size: 12px;\n font-weight: 500;\n line-height: 1.3;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.EnvSettingsSection_headerActions {\n width: 100px;\n}\n\n.EnvSettingsSection_field {\n min-width: 0;\n overflow: hidden;\n}\n\n.EnvSettingsSection_fieldInput {\n display: flex;\n box-sizing: border-box;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n /* Keep unfocused inputs out of drag text-select; focused field can still edit/select. */\n user-select: none;\n}\n\n.EnvSettingsSection_field:focus-within,\n.EnvSettingsSection_field:focus-within .EnvSettingsSection_fieldInput,\n.EnvSettingsSection_row[data-composer='true'] .EnvSettingsSection_fieldInput {\n user-select: text;\n}\n\n.EnvSettingsSection_actions {\n display: grid;\n grid-template-columns: repeat(3, 28px);\n gap: 4px;\n align-items: center;\n justify-content: end;\n width: 100px;\n margin-left: auto;\n flex-shrink: 0;\n}\n\n.EnvSettingsSection_slot {\n display: flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n}\n\n.EnvSettingsSection_slot[data-empty='true'] {\n visibility: hidden;\n pointer-events: none;\n}\n\n.EnvSettingsSection_iconButton {\n display: inline-flex;\n width: 28px;\n height: 28px;\n margin: 0;\n padding: 0;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n.EnvSettingsSection_iconButton:hover:not(:disabled) {\n background: var(--dsw-alias-fill-hover, rgba(127, 127, 127, 0.12));\n color: var(--dsw-alias-label-primary);\n}\n\n.EnvSettingsSection_iconButton:disabled {\n opacity: 0.45;\n cursor: default;\n}\n\n.EnvSettingsSection_iconButtonDanger:hover:not(:disabled) {\n color: var(--dsw-alias-label-danger, #c62828);\n}\n\n.EnvSettingsSection_iconButtonPrimary {\n color: var(--dsw-alias-brand-primary, #247bbf);\n}\n\n.EnvSettingsSection_restartBadge {\n display: inline-flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n color: var(--dsw-alias-label-warning, #c67c00);\n}\n\n.EnvSettingsSection_spinner {\n animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n to { transform: rotate(360deg); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .EnvSettingsSection_spinner {\n animation: none;\n }\n}\n";
625
+ var css = ".EnvSettingsSection_section {\n display: flex;\n flex-direction: column;\n gap: 16px;\n max-width: 760px;\n min-width: 0;\n color: var(--dsw-alias-label-primary);\n container-type: inline-size;\n}\n\n.EnvSettingsSection_heading {\n margin: 0;\n font-size: 18px;\n font-weight: 500;\n line-height: 1.3;\n}\n\n/* \u2014\u2014 dsh CLI: exactly two lines (label + path) \u2014\u2014 */\n.EnvSettingsSection_cli {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.EnvSettingsSection_cliHeading {\n margin: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.EnvSettingsSection_cliBody {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 28px;\n gap: 8px;\n align-items: center;\n height: 28px;\n}\n\n.EnvSettingsSection_cliPath {\n margin: 0;\n min-width: 0;\n font-size: 13px;\n line-height: 28px;\n color: var(--dsw-alias-label-tertiary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.EnvSettingsSection_cliSlot {\n display: flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n}\n\n.EnvSettingsSection_cliSlot[data-empty='true'] {\n visibility: hidden;\n pointer-events: none;\n}\n\n.EnvSettingsSection_notice {\n margin: 0;\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.EnvSettingsSection_notice[data-tone='error'] {\n color: var(--dsw-alias-label-danger, #c62828);\n}\n\n/* \u2014\u2014 variable rows: name | value | actions \u2014\u2014 */\n.EnvSettingsSection_header,\n.EnvSettingsSection_row {\n display: grid;\n grid-template-columns: minmax(0, 0.75fr) minmax(0, 1.25fr) 116px;\n gap: 10px;\n align-items: center;\n min-width: 0;\n}\n\n.EnvSettingsSection_row {\n padding: 4px 6px;\n margin: 0 -6px;\n user-select: none;\n}\n\n.EnvSettingsSection_row[data-selected='true'] {\n background: var(--dsw-alias-fill-hover, rgba(127, 127, 127, 0.1));\n}\n\n.EnvSettingsSection_row[data-composer='true'] {\n user-select: auto;\n}\n\n.EnvSettingsSection_header {\n padding: 0 0 2px;\n}\n\n.EnvSettingsSection_headerLabel {\n margin: 0;\n font-size: 12px;\n font-weight: 500;\n line-height: 1.3;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.EnvSettingsSection_headerActions {\n width: 116px;\n}\n\n/* Extremely narrow pane: value wraps under name; actions stay on the first row. */\n@container (max-width: 360px) {\n .EnvSettingsSection_header,\n .EnvSettingsSection_row {\n grid-template-columns: minmax(0, 1fr) 116px;\n align-items: start;\n row-gap: 8px;\n }\n\n .EnvSettingsSection_header {\n align-items: end;\n }\n\n .EnvSettingsSection_header > :nth-child(1),\n .EnvSettingsSection_row > :nth-child(1) {\n grid-column: 1;\n grid-row: 1;\n }\n\n .EnvSettingsSection_header > :nth-child(3),\n .EnvSettingsSection_row > :nth-child(3) {\n grid-column: 2;\n grid-row: 1;\n }\n\n .EnvSettingsSection_header > :nth-child(2),\n .EnvSettingsSection_row > :nth-child(2) {\n grid-column: 1 / -1;\n grid-row: 2;\n }\n\n .EnvSettingsSection_actions {\n margin-left: 0;\n }\n}\n\n.EnvSettingsSection_field {\n min-width: 0;\n overflow: hidden;\n}\n\n.EnvSettingsSection_fieldInput {\n display: flex;\n box-sizing: border-box;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n user-select: none;\n}\n\n.EnvSettingsSection_field:focus-within,\n.EnvSettingsSection_field:focus-within .EnvSettingsSection_fieldInput,\n.EnvSettingsSection_row[data-composer='true'] .EnvSettingsSection_fieldInput {\n user-select: text;\n}\n\n.EnvSettingsSection_actions {\n display: grid;\n grid-template-columns: repeat(4, 26px);\n gap: 2px;\n align-items: center;\n justify-content: end;\n width: 116px;\n margin-left: auto;\n flex-shrink: 0;\n}\n\n.EnvSettingsSection_slot {\n display: flex;\n width: 26px;\n height: 26px;\n align-items: center;\n justify-content: center;\n}\n\n.EnvSettingsSection_slot[data-empty='true'] {\n visibility: hidden;\n pointer-events: none;\n}\n\n.EnvSettingsSection_iconButton {\n display: inline-flex;\n width: 26px;\n height: 26px;\n margin: 0;\n padding: 0;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n.EnvSettingsSection_iconButton:hover:not(:disabled) {\n background: var(--dsw-alias-fill-hover, rgba(127, 127, 127, 0.12));\n color: var(--dsw-alias-label-primary);\n}\n\n.EnvSettingsSection_iconButton:disabled {\n opacity: 0.45;\n cursor: default;\n}\n\n.EnvSettingsSection_iconButtonDanger:hover:not(:disabled) {\n color: var(--dsw-alias-label-danger, #c62828);\n}\n\n.EnvSettingsSection_iconButtonPrimary {\n color: var(--dsw-alias-brand-primary, #247bbf);\n}\n\n.EnvSettingsSection_restartBadge {\n display: inline-flex;\n width: 26px;\n height: 26px;\n align-items: center;\n justify-content: center;\n color: var(--dsw-alias-label-warning, #c67c00);\n}\n\n.EnvSettingsSection_spinner {\n animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n to { transform: rotate(360deg); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .EnvSettingsSection_spinner {\n animation: none;\n }\n}\n\n.EnvSettingsSection_groups {\n display: flex;\n flex-direction: column;\n gap: 14px;\n min-width: 0;\n}\n\n.EnvSettingsSection_group {\n display: flex;\n flex-direction: column;\n gap: 10px;\n min-width: 0;\n}\n\n.EnvSettingsSection_group + .EnvSettingsSection_group {\n border-top: 0.5px solid var(--dsw-alias-border-l2, rgba(127, 127, 127, 0.25));\n padding-top: 14px;\n}\n\n.EnvSettingsSection_groupTitleRow {\n display: flex;\n align-items: center;\n gap: 8px;\n min-height: 36px;\n}\n\n.EnvSettingsSection_groupToggle {\n display: flex;\n flex: none;\n align-items: center;\n gap: 8px;\n border: 0;\n padding: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.EnvSettingsSection_groupToggle:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary, #247bbf);\n outline-offset: 2px;\n}\n\n.EnvSettingsSection_groupToggle > .EnvSettingsSection_chevron {\n transform: rotate(-90deg);\n}\n\n.EnvSettingsSection_groupToggle[aria-expanded='true'] > .EnvSettingsSection_chevron {\n transform: none;\n}\n\n.EnvSettingsSection_groupTitle {\n font-size: 14px;\n line-height: 22px;\n font-weight: 400;\n color: var(--dsw-alias-label-primary);\n}\n\n.EnvSettingsSection_headerEnd {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-left: auto;\n}\n\n.EnvSettingsSection_groupBody {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 0;\n}\n\n.EnvSettingsSection_modalField {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.EnvSettingsSection_modalCheck {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 12px;\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n";
630
626
  if (typeof document !== "undefined" && !document.getElementById("dsh-custom-env-css")) {
631
627
  const style = document.createElement("style");
632
628
  style.id = "dsh-custom-env-css";
633
629
  style.textContent = css;
634
630
  document.head.appendChild(style);
635
631
  }
636
- var EnvSettingsSection_default = { "section": "EnvSettingsSection_section", "heading": "EnvSettingsSection_heading", "cli": "EnvSettingsSection_cli", "cliHeading": "EnvSettingsSection_cliHeading", "cliBody": "EnvSettingsSection_cliBody", "cliPath": "EnvSettingsSection_cliPath", "cliSlot": "EnvSettingsSection_cliSlot", "notice": "EnvSettingsSection_notice", "empty": "EnvSettingsSection_empty", "list": "EnvSettingsSection_list", "header": "EnvSettingsSection_header", "row": "EnvSettingsSection_row", "headerLabel": "EnvSettingsSection_headerLabel", "headerActions": "EnvSettingsSection_headerActions", "field": "EnvSettingsSection_field", "fieldInput": "EnvSettingsSection_fieldInput", "actions": "EnvSettingsSection_actions", "slot": "EnvSettingsSection_slot", "iconButton": "EnvSettingsSection_iconButton", "iconButtonDanger": "EnvSettingsSection_iconButtonDanger", "iconButtonPrimary": "EnvSettingsSection_iconButtonPrimary", "restartBadge": "EnvSettingsSection_restartBadge", "spinner": "EnvSettingsSection_spinner" };
632
+ var EnvSettingsSection_default = { "section": "EnvSettingsSection_section", "heading": "EnvSettingsSection_heading", "cli": "EnvSettingsSection_cli", "cliHeading": "EnvSettingsSection_cliHeading", "cliBody": "EnvSettingsSection_cliBody", "cliPath": "EnvSettingsSection_cliPath", "cliSlot": "EnvSettingsSection_cliSlot", "notice": "EnvSettingsSection_notice", "header": "EnvSettingsSection_header", "row": "EnvSettingsSection_row", "headerLabel": "EnvSettingsSection_headerLabel", "headerActions": "EnvSettingsSection_headerActions", "actions": "EnvSettingsSection_actions", "field": "EnvSettingsSection_field", "fieldInput": "EnvSettingsSection_fieldInput", "slot": "EnvSettingsSection_slot", "iconButton": "EnvSettingsSection_iconButton", "iconButtonDanger": "EnvSettingsSection_iconButtonDanger", "iconButtonPrimary": "EnvSettingsSection_iconButtonPrimary", "restartBadge": "EnvSettingsSection_restartBadge", "spinner": "EnvSettingsSection_spinner", "groups": "EnvSettingsSection_groups", "group": "EnvSettingsSection_group", "groupTitleRow": "EnvSettingsSection_groupTitleRow", "groupToggle": "EnvSettingsSection_groupToggle", "chevron": "EnvSettingsSection_chevron", "groupTitle": "EnvSettingsSection_groupTitle", "headerEnd": "EnvSettingsSection_headerEnd", "groupBody": "EnvSettingsSection_groupBody", "modalField": "EnvSettingsSection_modalField", "modalCheck": "EnvSettingsSection_modalCheck" };
637
633
 
638
634
  // src/client/EnvSettingsSection.tsx
639
635
  var import_jsx_runtime = require("react/jsx-runtime");
640
636
  var NAME_OK2 = /^[A-Za-z_][A-Za-z0-9_]*$/u;
641
- var COMPOSER_ID = "";
642
637
  var PENDING_PREFIX = "pending:";
638
+ var COMPOSER_PREFIX = "composer:";
643
639
  function isPendingId(id) {
644
640
  return id.startsWith(PENDING_PREFIX);
645
641
  }
642
+ function composerId(groupId) {
643
+ return `${COMPOSER_PREFIX}${groupId}`;
644
+ }
645
+ function isComposerId(id) {
646
+ return id.startsWith(COMPOSER_PREFIX);
647
+ }
648
+ function rowsByGroup(groups, entries, pendingOrder, pendingGroup) {
649
+ const saved = new Map(groups.map((group) => [group.id, []]));
650
+ const pending = new Map(groups.map((group) => [group.id, []]));
651
+ for (const entry of entries) {
652
+ (saved.get(saved.has(entry.groupId) ? entry.groupId : DEFAULT_GROUP_ID) ?? saved.get(DEFAULT_GROUP_ID)).push(entry);
653
+ }
654
+ for (const id of pendingOrder) {
655
+ const groupId = pendingGroup[id] ?? DEFAULT_GROUP_ID;
656
+ (pending.get(pending.has(groupId) ? groupId : DEFAULT_GROUP_ID) ?? pending.get(DEFAULT_GROUP_ID)).push(id);
657
+ }
658
+ return { saved, pending };
659
+ }
646
660
  function IconAction({
647
661
  label,
648
662
  disabled,
@@ -675,14 +689,37 @@ function RowActions({
675
689
  canConfirm,
676
690
  pending = false,
677
691
  showRemove = true,
692
+ moveItems,
678
693
  onConfirm,
679
694
  onCancel,
680
695
  onRestore,
681
- onRemove
696
+ onRemove,
697
+ onMove
682
698
  }) {
699
+ const [moveOpen, setMoveOpen] = (0, import_react.useState)(false);
700
+ const showMove = moveItems !== void 0 && moveItems.length > 0 && onMove !== void 0;
683
701
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.actions, role: "group", "aria-label": t("field.actions"), children: [
684
702
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slot, { empty: !dirty && !pending, children: dirty ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconAction, { label: loading ? t("panel.loading") : t("action.confirm"), primary: true, disabled: busy || !canConfirm, onClick: onConfirm, children: loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconLoadingOutlineRegular, { className: EnvSettingsSection_default.spinner, size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconCheckOutlineRegular, { size: 14 }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tooltip, { portal: true, label: t("status.restartRequired"), side: "top", delayMs: 400, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: EnvSettingsSection_default.restartBadge, role: "img", "aria-label": t("status.restartRequired"), title: t("status.restartRequired"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconWarningOutlineRegular, { size: 14 }) }) }) }),
685
703
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slot, { empty: !dirty && !pending, children: dirty ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconAction, { label: t("action.cancel"), disabled: busy, onClick: onCancel, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconCloseOutlineRegular, { size: 14 }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconAction, { label: t("action.restore"), disabled: busy, onClick: onRestore, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconRefreshOutlineRegular, { size: 14 }) }) }),
704
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slot, { empty: !showMove, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
705
+ import_dsh_client_ui_primitives.Menu,
706
+ {
707
+ open: moveOpen,
708
+ onClose: () => {
709
+ setMoveOpen(false);
710
+ },
711
+ items: (moveItems ?? []).map((item) => ({ id: item.id, label: item.label })),
712
+ onSelect: (groupId) => {
713
+ setMoveOpen(false);
714
+ onMove?.(groupId);
715
+ },
716
+ align: "end",
717
+ portal: true,
718
+ anchor: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconAction, { label: t("action.moveGroup"), disabled: busy, onClick: () => {
719
+ setMoveOpen((value) => !value);
720
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconFolderOpenOutlineRegular, { size: 14 }) })
721
+ }
722
+ ) }),
686
723
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slot, { empty: !showRemove, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconAction, { label: t("action.remove"), danger: true, disabled: busy, onClick: onRemove, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconTrashOutlineRegular, { size: 14 }) }) })
687
724
  ] });
688
725
  }
@@ -718,23 +755,33 @@ function EnvSettingsSection({
718
755
  upsert,
719
756
  remove,
720
757
  restore,
758
+ moveEntry,
759
+ createGroup,
760
+ renameGroup,
761
+ deleteGroup,
721
762
  cliStatus,
722
763
  cliInstall
723
764
  }) {
724
765
  const [snapshot, setSnapshot] = (0, import_react.useState)();
725
766
  const [cli, setCli] = (0, import_react.useState)();
726
- const [drafts, setDrafts] = (0, import_react.useState)({ [COMPOSER_ID]: { name: "", value: "" } });
767
+ const [drafts, setDrafts] = (0, import_react.useState)({});
727
768
  const [pendingOrder, setPendingOrder] = (0, import_react.useState)([]);
769
+ const [pendingGroup, setPendingGroup] = (0, import_react.useState)({});
770
+ const [openGroups, setOpenGroups] = (0, import_react.useState)({ [DEFAULT_GROUP_ID]: true });
728
771
  const [busyTarget, setBusyTarget] = (0, import_react.useState)();
729
772
  const [error, setError] = (0, import_react.useState)();
730
773
  const [nameError, setNameError] = (0, import_react.useState)(false);
731
774
  const [pendingRemove, setPendingRemove] = (0, import_react.useState)();
775
+ const [groupModal, setGroupModal] = (0, import_react.useState)();
776
+ const [groupTitleDraft, setGroupTitleDraft] = (0, import_react.useState)("");
732
777
  const [selectedIds, setSelectedIds] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
733
778
  const pendingSeq = (0, import_react.useRef)(0);
734
779
  const draftsRef = (0, import_react.useRef)(drafts);
735
780
  draftsRef.current = drafts;
736
781
  const pendingOrderRef = (0, import_react.useRef)(pendingOrder);
737
782
  pendingOrderRef.current = pendingOrder;
783
+ const pendingGroupRef = (0, import_react.useRef)(pendingGroup);
784
+ pendingGroupRef.current = pendingGroup;
738
785
  const selectedRef = (0, import_react.useRef)(selectedIds);
739
786
  selectedRef.current = selectedIds;
740
787
  const anchorIdRef = (0, import_react.useRef)(void 0);
@@ -743,35 +790,61 @@ function EnvSettingsSection({
743
790
  const dragPendingCrossRef = (0, import_react.useRef)(false);
744
791
  const pendingRemoveRef = (0, import_react.useRef)(pendingRemove);
745
792
  pendingRemoveRef.current = pendingRemove;
793
+ const groupModalRef = (0, import_react.useRef)(groupModal);
794
+ groupModalRef.current = groupModal;
746
795
  const busy = busyTarget !== void 0;
796
+ const groups = snapshot?.groups ?? [{ id: DEFAULT_GROUP_ID, title: t("group.defaultTitle") }];
747
797
  const entries = snapshot?.entries ?? [];
748
798
  const taken = new Set(entries.map((entry) => entry.name));
749
- const selectableOrder = [...entries.map((entry) => entry.id), ...pendingOrder];
799
+ const grouped = rowsByGroup(groups, entries, pendingOrder, pendingGroup);
800
+ const selectableOrder = groups.flatMap((group) => [
801
+ ...(grouped.saved.get(group.id) ?? []).map((entry) => entry.id),
802
+ ...grouped.pending.get(group.id) ?? []
803
+ ]);
750
804
  const selectableOrderRef = (0, import_react.useRef)(selectableOrder);
751
805
  selectableOrderRef.current = selectableOrder;
752
- const keepPendingDrafts = (current, dropId) => {
806
+ const groupLabel = (group) => group.id === DEFAULT_GROUP_ID ? t("group.defaultTitle") : group.title;
807
+ const keepTransientDrafts = (current, dropId) => {
753
808
  const kept = {};
754
809
  for (const [id, draft] of Object.entries(current)) {
755
- if (!isPendingId(id) || id === dropId) continue;
756
- kept[id] = draft;
810
+ if (isComposerId(id)) kept[id] = draft;
811
+ else if (isPendingId(id) && id !== dropId) kept[id] = draft;
757
812
  }
758
813
  return kept;
759
814
  };
815
+ const emptyComposers = (nextGroups, current) => Object.fromEntries(nextGroups.map((group) => {
816
+ const id = composerId(group.id);
817
+ return [id, current[id] ?? { name: "", value: "" }];
818
+ }));
819
+ const forgetPendingId = (id) => {
820
+ setPendingOrder((order) => order.filter((item) => item !== id));
821
+ setPendingGroup((previous) => {
822
+ if (previous[id] === void 0) return previous;
823
+ const next = { ...previous };
824
+ delete next[id];
825
+ return next;
826
+ });
827
+ setSelectedIds((previous) => {
828
+ if (!previous.has(id)) return previous;
829
+ const next = new Set(previous);
830
+ next.delete(id);
831
+ return next;
832
+ });
833
+ if (anchorIdRef.current === id) anchorIdRef.current = void 0;
834
+ };
760
835
  const applySnapshot = (next, dropPendingId) => {
761
836
  setSnapshot(next);
762
- if (dropPendingId !== void 0) {
763
- setPendingOrder((order) => order.filter((item) => item !== dropPendingId));
764
- setSelectedIds((previous) => {
765
- if (!previous.has(dropPendingId)) return previous;
766
- const ids = new Set(previous);
767
- ids.delete(dropPendingId);
768
- return ids;
769
- });
770
- if (anchorIdRef.current === dropPendingId) anchorIdRef.current = void 0;
771
- }
837
+ setOpenGroups((previous) => {
838
+ const merged = { ...previous };
839
+ for (const group of next.groups) {
840
+ if (merged[group.id] === void 0) merged[group.id] = group.id === DEFAULT_GROUP_ID;
841
+ }
842
+ return merged;
843
+ });
844
+ if (dropPendingId !== void 0) forgetPendingId(dropPendingId);
772
845
  setDrafts((current) => ({
773
- [COMPOSER_ID]: { name: "", value: "" },
774
- ...keepPendingDrafts(current, dropPendingId),
846
+ ...emptyComposers(next.groups, current),
847
+ ...keepTransientDrafts(current, dropPendingId),
775
848
  ...Object.fromEntries(next.entries.map((entry) => [entry.id, { name: entry.name, value: entry.value }]))
776
849
  }));
777
850
  };
@@ -782,14 +855,7 @@ function EnvSettingsSection({
782
855
  }));
783
856
  };
784
857
  const dropPending = (id) => {
785
- setPendingOrder((order) => order.filter((item) => item !== id));
786
- setSelectedIds((previous) => {
787
- if (!previous.has(id)) return previous;
788
- const next = new Set(previous);
789
- next.delete(id);
790
- return next;
791
- });
792
- if (anchorIdRef.current === id) anchorIdRef.current = void 0;
858
+ forgetPendingId(id);
793
859
  setDrafts((previous) => {
794
860
  const next = { ...previous };
795
861
  delete next[id];
@@ -832,11 +898,13 @@ function EnvSettingsSection({
832
898
  setError(void 0);
833
899
  try {
834
900
  setPendingOrder([]);
901
+ setPendingGroup({});
835
902
  clearSelection();
836
903
  const next = await load();
837
904
  setSnapshot(next);
905
+ setOpenGroups(Object.fromEntries(next.groups.map((group) => [group.id, group.id === DEFAULT_GROUP_ID])));
838
906
  setDrafts({
839
- [COMPOSER_ID]: { name: "", value: "" },
907
+ ...emptyComposers(next.groups, {}),
840
908
  ...Object.fromEntries(next.entries.map((entry) => [entry.id, { name: entry.name, value: entry.value }]))
841
909
  });
842
910
  try {
@@ -854,13 +922,12 @@ function EnvSettingsSection({
854
922
  void refresh();
855
923
  }, [refresh]);
856
924
  (0, import_react.useEffect)(() => {
857
- const allowed = new Set(selectableOrder);
858
- pruneSelection(allowed);
925
+ pruneSelection(new Set(selectableOrder));
859
926
  }, [selectableOrder.join("\0")]);
860
927
  const stopEditingGesture = () => {
861
928
  clearDomTextSelection();
862
929
  const active = document.activeElement;
863
- if (active instanceof HTMLInputElement && active.closest(`.${EnvSettingsSection_default.list}`) !== null) active.blur();
930
+ if (active instanceof HTMLInputElement && active.closest(`.${EnvSettingsSection_default.groups}`) !== null) active.blur();
864
931
  };
865
932
  const applyDragOverRow = (id) => {
866
933
  if (dragModeRef.current === void 0) return;
@@ -899,7 +966,7 @@ function EnvSettingsSection({
899
966
  if (id !== void 0) applyDragOverRow(id);
900
967
  };
901
968
  const onPointerDown = (event) => {
902
- if (selectedRef.current.size === 0 || pendingRemoveRef.current !== void 0) return;
969
+ if (selectedRef.current.size === 0 || pendingRemoveRef.current !== void 0 || groupModalRef.current !== void 0) return;
903
970
  if (!(event.target instanceof Element)) return;
904
971
  if (event.target.closest("[data-env-row]") !== null) return;
905
972
  clearSelection();
@@ -955,37 +1022,42 @@ function EnvSettingsSection({
955
1022
  }
956
1023
  if (draft.value === "") return;
957
1024
  setNameError(false);
958
- void run(`pending:${id}`, () => upsert(COMPOSER_ID, name, draft.value), id);
1025
+ const groupId = pendingGroup[id] ?? DEFAULT_GROUP_ID;
1026
+ void run(`pending:${id}`, () => upsert("", name, draft.value, groupId), id);
959
1027
  };
960
- const typeIntoComposer = (patch, focus) => {
1028
+ const typeIntoComposer = (groupId, patch, focus) => {
961
1029
  setNameError(false);
962
- const base = draftsRef.current[COMPOSER_ID] ?? { name: "", value: "" };
1030
+ const key = composerId(groupId);
1031
+ const base = draftsRef.current[key] ?? { name: "", value: "" };
963
1032
  const next = { name: patch.name ?? base.name, value: patch.value ?? base.value };
964
1033
  const id = `${PENDING_PREFIX}${++pendingSeq.current}`;
965
1034
  setDrafts((previous) => ({
966
1035
  ...previous,
967
1036
  [id]: next,
968
- [COMPOSER_ID]: { name: "", value: "" }
1037
+ [key]: { name: "", value: "" }
969
1038
  }));
970
1039
  setPendingOrder((order) => [...order, id]);
1040
+ setPendingGroup((previous) => ({ ...previous, [id]: groupId }));
971
1041
  queueMicrotask(() => {
972
1042
  document.getElementById(`env-${focus}-${id}`)?.focus();
973
1043
  });
974
1044
  };
975
- const appendPendingPairs = (pairs) => {
1045
+ const appendPendingPairs = (groupId, pairs) => {
976
1046
  if (pairs.length === 0) return;
977
1047
  setNameError(false);
978
1048
  const previous = draftsRef.current;
979
1049
  const order = pendingOrderRef.current;
980
- const nextDrafts = { ...previous, [COMPOSER_ID]: { name: "", value: "" } };
981
- const savedByName = new Map(entries.map((entry) => [entry.name, entry.id]));
1050
+ const groupsByPending = pendingGroupRef.current;
1051
+ const nextDrafts = { ...previous, [composerId(groupId)]: { name: "", value: "" } };
982
1052
  const existingPendingByName = /* @__PURE__ */ new Map();
983
1053
  for (const id of order) {
984
1054
  const draft = previous[id];
985
1055
  if (draft !== void 0) existingPendingByName.set(draft.name.trim(), id);
986
1056
  }
1057
+ const savedByName = new Map(entries.map((entry) => [entry.name, entry.id]));
987
1058
  const added = [];
988
- const dropPending2 = /* @__PURE__ */ new Set();
1059
+ const dropPendingIds = /* @__PURE__ */ new Set();
1060
+ const nextPendingGroup = { ...groupsByPending };
989
1061
  for (const pair of pairs) {
990
1062
  const savedId = savedByName.get(pair.name);
991
1063
  if (savedId !== void 0) {
@@ -993,7 +1065,8 @@ function EnvSettingsSection({
993
1065
  const pendingDup = existingPendingByName.get(pair.name);
994
1066
  if (pendingDup !== void 0) {
995
1067
  delete nextDrafts[pendingDup];
996
- dropPending2.add(pendingDup);
1068
+ dropPendingIds.add(pendingDup);
1069
+ delete nextPendingGroup[pendingDup];
997
1070
  existingPendingByName.delete(pair.name);
998
1071
  }
999
1072
  continue;
@@ -1006,19 +1079,21 @@ function EnvSettingsSection({
1006
1079
  const id = `${PENDING_PREFIX}${++pendingSeq.current}`;
1007
1080
  nextDrafts[id] = { name: pair.name, value: pair.value };
1008
1081
  existingPendingByName.set(pair.name, id);
1082
+ nextPendingGroup[id] = groupId;
1009
1083
  added.push(id);
1010
1084
  }
1011
1085
  setDrafts(nextDrafts);
1012
- if (added.length > 0 || dropPending2.size > 0) {
1013
- setPendingOrder([...order.filter((id) => !dropPending2.has(id)), ...added]);
1086
+ setPendingGroup(nextPendingGroup);
1087
+ if (added.length > 0 || dropPendingIds.size > 0) {
1088
+ setPendingOrder([...order.filter((id) => !dropPendingIds.has(id)), ...added]);
1014
1089
  }
1015
1090
  };
1016
- const onComposerPaste = (event) => {
1091
+ const onComposerPaste = (groupId, event) => {
1017
1092
  const text = event.clipboardData.getData("text");
1018
1093
  const pairs = parseEnvPaste(text);
1019
1094
  if (pairs.length === 0) return;
1020
1095
  event.preventDefault();
1021
- appendPendingPairs(pairs);
1096
+ appendPendingPairs(groupId, pairs);
1022
1097
  };
1023
1098
  const onRowMouseDown = (id, event) => {
1024
1099
  if (event.button !== 0) return;
@@ -1053,11 +1128,246 @@ function EnvSettingsSection({
1053
1128
  dragVisitedRef.current = /* @__PURE__ */ new Set([id]);
1054
1129
  dragPendingCrossRef.current = true;
1055
1130
  };
1056
- const composer = drafts[COMPOSER_ID] ?? { name: "", value: "" };
1131
+ const submitGroupModal = () => {
1132
+ if (groupModal === void 0 || busyTarget !== void 0) return;
1133
+ if (groupModal.kind === "create" || groupModal.kind === "rename") {
1134
+ const title = groupTitleDraft.trim();
1135
+ if (title === "") return;
1136
+ setBusyTarget("group");
1137
+ setError(void 0);
1138
+ void (async () => {
1139
+ try {
1140
+ if (groupModal.kind === "create") {
1141
+ const group = await createGroup(title);
1142
+ const next = await load();
1143
+ applySnapshot(next);
1144
+ setOpenGroups((previous) => ({ ...previous, [group.id]: true }));
1145
+ } else {
1146
+ await renameGroup(groupModal.id, title);
1147
+ applySnapshot(await load());
1148
+ }
1149
+ setGroupModal(void 0);
1150
+ } catch (err) {
1151
+ setError(formatRemoteError(err, t));
1152
+ } finally {
1153
+ setBusyTarget(void 0);
1154
+ }
1155
+ })();
1156
+ return;
1157
+ }
1158
+ setBusyTarget("group");
1159
+ setError(void 0);
1160
+ void (async () => {
1161
+ try {
1162
+ applySnapshot(await deleteGroup(groupModal.id, groupModal.moveToDefault));
1163
+ setGroupModal(void 0);
1164
+ } catch (err) {
1165
+ setError(formatRemoteError(err, t));
1166
+ } finally {
1167
+ setBusyTarget(void 0);
1168
+ }
1169
+ })();
1170
+ };
1057
1171
  const cliInstalling = busyTarget === "cli";
1058
1172
  const cliBooting = busyTarget === "boot" || cli === void 0;
1059
1173
  const showCliAction = cli?.canInstall === true || cliInstalling;
1060
1174
  const notice = error ?? (nameError ? t("error.invalidName") : void 0) ?? (cli?.reason === "could-not-resolve-expected-cli" ? t("error.cliUnresolved") : void 0) ?? (cli?.reason === "unsupported-platform" ? t("error.cliUnsupported") : void 0);
1175
+ const moveTargets = (currentGroupId) => groups.filter((group) => group.id !== currentGroupId).map((group) => ({ id: group.id, label: groupLabel(group) }));
1176
+ const renderRow = (entry) => {
1177
+ const draft = drafts[entry.id] ?? { name: entry.name, value: entry.value };
1178
+ const dirty = draft.name !== entry.name || draft.value !== entry.value;
1179
+ const pendingKind = entry.kind === "added" || entry.kind === "modified";
1180
+ const loading = busyTarget === `row:${entry.id}`;
1181
+ const nextName = draft.name.trim();
1182
+ const canConfirm = NAME_OK2.test(nextName) && draft.value !== "" && !(nextName !== entry.name && taken.has(nextName));
1183
+ const selected = selectedIds.has(entry.id);
1184
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1185
+ "div",
1186
+ {
1187
+ className: EnvSettingsSection_default.row,
1188
+ "data-env-row": entry.id,
1189
+ "data-selected": selected ? "true" : "false",
1190
+ "aria-selected": selected,
1191
+ onMouseDown: (event) => {
1192
+ onRowMouseDown(entry.id, event);
1193
+ },
1194
+ children: [
1195
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1196
+ Field,
1197
+ {
1198
+ id: `env-name-${entry.id}`,
1199
+ value: draft.name,
1200
+ disabled: busy,
1201
+ title: draft.name,
1202
+ invalid: nameError && dirty,
1203
+ label: t("field.name"),
1204
+ rowSelectGestures: true,
1205
+ onChange: (name) => {
1206
+ setNameError(false);
1207
+ setDraft(entry.id, { name });
1208
+ }
1209
+ }
1210
+ ),
1211
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1212
+ Field,
1213
+ {
1214
+ id: `env-value-${entry.id}`,
1215
+ value: draft.value,
1216
+ disabled: busy,
1217
+ title: draft.value,
1218
+ label: t("field.value"),
1219
+ rowSelectGestures: true,
1220
+ onChange: (value) => {
1221
+ setDraft(entry.id, { value });
1222
+ }
1223
+ }
1224
+ ),
1225
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1226
+ RowActions,
1227
+ {
1228
+ t,
1229
+ busy,
1230
+ loading,
1231
+ dirty: dirty || loading,
1232
+ canConfirm,
1233
+ pending: pendingKind && !dirty ? true : false,
1234
+ moveItems: moveTargets(entry.groupId),
1235
+ onConfirm: () => {
1236
+ saveExisting(entry.id, entry.name);
1237
+ },
1238
+ onCancel: () => {
1239
+ setNameError(false);
1240
+ setDraft(entry.id, { name: entry.name, value: entry.value });
1241
+ },
1242
+ onRestore: () => {
1243
+ void run(`row:${entry.id}`, () => restore(entry.id));
1244
+ },
1245
+ onRemove: () => {
1246
+ setPendingRemove({ id: entry.id, name: entry.name });
1247
+ },
1248
+ onMove: (groupId) => {
1249
+ void run(`row:${entry.id}`, () => moveEntry(entry.id, groupId));
1250
+ }
1251
+ }
1252
+ )
1253
+ ]
1254
+ },
1255
+ entry.id
1256
+ );
1257
+ };
1258
+ const renderPending = (id, groupId) => {
1259
+ const draft = drafts[id] ?? { name: "", value: "" };
1260
+ const loading = busyTarget === `pending:${id}`;
1261
+ const nextName = draft.name.trim();
1262
+ const canConfirm = NAME_OK2.test(nextName) && draft.value !== "" && !taken.has(nextName);
1263
+ const selected = selectedIds.has(id);
1264
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1265
+ "div",
1266
+ {
1267
+ className: EnvSettingsSection_default.row,
1268
+ "data-env-row": id,
1269
+ "data-selected": selected ? "true" : "false",
1270
+ "aria-selected": selected,
1271
+ onMouseDown: (event) => {
1272
+ onRowMouseDown(id, event);
1273
+ },
1274
+ children: [
1275
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1276
+ Field,
1277
+ {
1278
+ id: `env-name-${id}`,
1279
+ value: draft.name,
1280
+ disabled: busy,
1281
+ title: draft.name,
1282
+ invalid: nameError,
1283
+ label: t("field.name"),
1284
+ rowSelectGestures: true,
1285
+ onChange: (name) => {
1286
+ setNameError(false);
1287
+ setDraft(id, { name });
1288
+ }
1289
+ }
1290
+ ),
1291
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1292
+ Field,
1293
+ {
1294
+ id: `env-value-${id}`,
1295
+ value: draft.value,
1296
+ disabled: busy,
1297
+ title: draft.value,
1298
+ label: t("field.value"),
1299
+ rowSelectGestures: true,
1300
+ onChange: (value) => {
1301
+ setDraft(id, { value });
1302
+ }
1303
+ }
1304
+ ),
1305
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1306
+ RowActions,
1307
+ {
1308
+ t,
1309
+ busy,
1310
+ loading,
1311
+ dirty: true,
1312
+ canConfirm,
1313
+ showRemove: false,
1314
+ onConfirm: () => {
1315
+ savePending(id);
1316
+ },
1317
+ onCancel: () => {
1318
+ setNameError(false);
1319
+ dropPending(id);
1320
+ },
1321
+ onRestore: () => void 0,
1322
+ onRemove: () => void 0
1323
+ }
1324
+ )
1325
+ ]
1326
+ },
1327
+ id
1328
+ );
1329
+ };
1330
+ const renderComposer = (groupId) => {
1331
+ const key = composerId(groupId);
1332
+ const composer = drafts[key] ?? { name: "", value: "" };
1333
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.row, "data-composer": "true", children: [
1334
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1335
+ Field,
1336
+ {
1337
+ id: `env-new-name-${groupId}`,
1338
+ value: composer.name,
1339
+ disabled: busy,
1340
+ placeholder: t("field.namePlaceholder"),
1341
+ title: composer.name || t("field.namePlaceholder"),
1342
+ label: t("field.name"),
1343
+ onChange: (name) => {
1344
+ typeIntoComposer(groupId, { name }, "name");
1345
+ },
1346
+ onPaste: (event) => {
1347
+ onComposerPaste(groupId, event);
1348
+ }
1349
+ }
1350
+ ),
1351
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1352
+ Field,
1353
+ {
1354
+ id: `env-new-value-${groupId}`,
1355
+ value: composer.value,
1356
+ disabled: busy,
1357
+ placeholder: t("field.valuePlaceholder"),
1358
+ title: composer.value || t("field.valuePlaceholder"),
1359
+ label: t("field.value"),
1360
+ onChange: (value) => {
1361
+ typeIntoComposer(groupId, { value }, "value");
1362
+ },
1363
+ onPaste: (event) => {
1364
+ onComposerPaste(groupId, event);
1365
+ }
1366
+ }
1367
+ ),
1368
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: EnvSettingsSection_default.actions, "aria-hidden": "true" })
1369
+ ] });
1370
+ };
1061
1371
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.section, children: [
1062
1372
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: EnvSettingsSection_default.heading, children: t("panel.title") }),
1063
1373
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: EnvSettingsSection_default.cli, "aria-label": t("cli.title"), children: [
@@ -1097,197 +1407,83 @@ function EnvSettingsSection({
1097
1407
  ] })
1098
1408
  ] }),
1099
1409
  notice !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.notice, "data-tone": error !== void 0 || nameError ? "error" : void 0, role: "status", children: notice }) : null,
1100
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.list, children: [
1101
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.header, children: [
1102
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.headerLabel, children: t("field.name") }),
1103
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.headerLabel, children: t("field.value") }),
1104
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: EnvSettingsSection_default.headerActions, "aria-hidden": "true" })
1105
- ] }),
1106
- snapshot !== void 0 && entries.length === 0 && pendingOrder.length === 0 && notice === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.empty, children: t("panel.empty") }) : null,
1107
- entries.map((entry) => {
1108
- const draft = drafts[entry.id] ?? { name: entry.name, value: entry.value };
1109
- const dirty = draft.name !== entry.name || draft.value !== entry.value;
1110
- const pending = entry.kind === "added" || entry.kind === "modified";
1111
- const loading = busyTarget === `row:${entry.id}`;
1112
- const nextName = draft.name.trim();
1113
- const canConfirm = NAME_OK2.test(nextName) && draft.value !== "" && !(nextName !== entry.name && taken.has(nextName));
1114
- const selected = selectedIds.has(entry.id);
1115
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1116
- "div",
1117
- {
1118
- className: EnvSettingsSection_default.row,
1119
- "data-env-row": entry.id,
1120
- "data-selected": selected ? "true" : "false",
1121
- "aria-selected": selected,
1122
- onMouseDown: (event) => {
1123
- onRowMouseDown(entry.id, event);
1124
- },
1125
- children: [
1126
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1127
- Field,
1128
- {
1129
- id: `env-name-${entry.id}`,
1130
- value: draft.name,
1131
- disabled: busy,
1132
- title: draft.name,
1133
- invalid: nameError && dirty,
1134
- label: t("field.name"),
1135
- rowSelectGestures: true,
1136
- onChange: (name) => {
1137
- setNameError(false);
1138
- setDraft(entry.id, { name });
1139
- }
1140
- }
1141
- ),
1142
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1143
- Field,
1144
- {
1145
- id: `env-value-${entry.id}`,
1146
- value: draft.value,
1147
- disabled: busy,
1148
- title: draft.value,
1149
- label: t("field.value"),
1150
- rowSelectGestures: true,
1151
- onChange: (value) => {
1152
- setDraft(entry.id, { value });
1153
- }
1154
- }
1155
- ),
1156
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1157
- RowActions,
1158
- {
1159
- t,
1160
- busy,
1161
- loading,
1162
- dirty: dirty || loading,
1163
- canConfirm,
1164
- pending,
1165
- onConfirm: () => {
1166
- saveExisting(entry.id, entry.name);
1167
- },
1168
- onCancel: () => {
1169
- setNameError(false);
1170
- setDraft(entry.id, { name: entry.name, value: entry.value });
1171
- },
1172
- onRestore: () => {
1173
- void run(`row:${entry.id}`, () => restore(entry.id));
1174
- },
1175
- onRemove: () => {
1176
- setPendingRemove({ id: entry.id, name: entry.name });
1177
- }
1178
- }
1179
- )
1180
- ]
1181
- },
1182
- entry.id
1183
- );
1184
- }),
1185
- pendingOrder.map((id) => {
1186
- const draft = drafts[id] ?? { name: "", value: "" };
1187
- const loading = busyTarget === `pending:${id}`;
1188
- const nextName = draft.name.trim();
1189
- const canConfirm = NAME_OK2.test(nextName) && draft.value !== "" && !taken.has(nextName);
1190
- const selected = selectedIds.has(id);
1191
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1192
- "div",
1193
- {
1194
- className: EnvSettingsSection_default.row,
1195
- "data-env-row": id,
1196
- "data-selected": selected ? "true" : "false",
1197
- "aria-selected": selected,
1198
- onMouseDown: (event) => {
1199
- onRowMouseDown(id, event);
1200
- },
1201
- children: [
1202
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1203
- Field,
1204
- {
1205
- id: `env-name-${id}`,
1206
- value: draft.name,
1207
- disabled: busy,
1208
- title: draft.name,
1209
- invalid: nameError,
1210
- label: t("field.name"),
1211
- rowSelectGestures: true,
1212
- onChange: (name) => {
1213
- setNameError(false);
1214
- setDraft(id, { name });
1215
- }
1216
- }
1217
- ),
1218
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1219
- Field,
1220
- {
1221
- id: `env-value-${id}`,
1222
- value: draft.value,
1223
- disabled: busy,
1224
- title: draft.value,
1225
- label: t("field.value"),
1226
- rowSelectGestures: true,
1227
- onChange: (value) => {
1228
- setDraft(id, { value });
1229
- }
1230
- }
1231
- ),
1232
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1233
- RowActions,
1234
- {
1235
- t,
1236
- busy,
1237
- loading,
1238
- dirty: true,
1239
- canConfirm,
1240
- showRemove: false,
1241
- onConfirm: () => {
1242
- savePending(id);
1243
- },
1244
- onCancel: () => {
1245
- setNameError(false);
1246
- dropPending(id);
1247
- },
1248
- onRestore: () => void 0,
1249
- onRemove: () => void 0
1250
- }
1251
- )
1252
- ]
1253
- },
1254
- id
1255
- );
1256
- }),
1257
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.row, "data-composer": "true", children: [
1258
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1259
- Field,
1260
- {
1261
- id: "env-new-name",
1262
- value: composer.name,
1263
- disabled: busy,
1264
- placeholder: t("field.namePlaceholder"),
1265
- title: composer.name || t("field.namePlaceholder"),
1266
- label: t("field.name"),
1267
- onChange: (name) => {
1268
- typeIntoComposer({ name }, "name");
1269
- },
1270
- onPaste: onComposerPaste
1271
- }
1272
- ),
1273
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1274
- Field,
1275
- {
1276
- id: "env-new-value",
1277
- value: composer.value,
1278
- disabled: busy,
1279
- placeholder: t("field.valuePlaceholder"),
1280
- title: composer.value || t("field.valuePlaceholder"),
1281
- label: t("field.value"),
1282
- onChange: (value) => {
1283
- typeIntoComposer({ value }, "value");
1284
- },
1285
- onPaste: onComposerPaste
1286
- }
1287
- ),
1288
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: EnvSettingsSection_default.actions, "aria-hidden": "true" })
1289
- ] })
1290
- ] }),
1410
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: EnvSettingsSection_default.groups, children: groups.map((group) => {
1411
+ const expanded = openGroups[group.id] === true;
1412
+ const groupEntries = grouped.saved.get(group.id) ?? [];
1413
+ const groupPending = grouped.pending.get(group.id) ?? [];
1414
+ const isDefault = group.id === DEFAULT_GROUP_ID;
1415
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: EnvSettingsSection_default.group, children: [
1416
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.groupTitleRow, children: [
1417
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1418
+ "button",
1419
+ {
1420
+ type: "button",
1421
+ className: EnvSettingsSection_default.groupToggle,
1422
+ "aria-expanded": expanded,
1423
+ onClick: () => {
1424
+ setOpenGroups((previous) => ({ ...previous, [group.id]: !expanded }));
1425
+ },
1426
+ children: [
1427
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: EnvSettingsSection_default.chevron, size: 12, "aria-hidden": "true" }),
1428
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: EnvSettingsSection_default.groupTitle, children: groupLabel(group) })
1429
+ ]
1430
+ }
1431
+ ),
1432
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: EnvSettingsSection_default.headerEnd, children: isDefault ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1433
+ IconAction,
1434
+ {
1435
+ label: t("action.createGroup"),
1436
+ disabled: busy,
1437
+ onClick: () => {
1438
+ setGroupTitleDraft("");
1439
+ setGroupModal({ kind: "create" });
1440
+ },
1441
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconPlusOutlineRegular, { size: 14 })
1442
+ }
1443
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1444
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1445
+ IconAction,
1446
+ {
1447
+ label: t("action.renameGroup"),
1448
+ disabled: busy,
1449
+ onClick: () => {
1450
+ setGroupTitleDraft(group.title);
1451
+ setGroupModal({ kind: "rename", id: group.id, title: group.title });
1452
+ },
1453
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconEditOutlineRegular, { size: 14 })
1454
+ }
1455
+ ),
1456
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1457
+ IconAction,
1458
+ {
1459
+ label: t("action.deleteGroup"),
1460
+ danger: true,
1461
+ disabled: busy,
1462
+ onClick: () => {
1463
+ setGroupModal({
1464
+ kind: "delete",
1465
+ id: group.id,
1466
+ title: group.title,
1467
+ moveToDefault: true
1468
+ });
1469
+ },
1470
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconTrashOutlineRegular, { size: 14 })
1471
+ }
1472
+ )
1473
+ ] }) })
1474
+ ] }),
1475
+ expanded ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.groupBody, children: [
1476
+ isDefault ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.header, children: [
1477
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.headerLabel, children: t("field.name") }),
1478
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.headerLabel, children: t("field.value") }),
1479
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: EnvSettingsSection_default.headerActions, "aria-hidden": "true" })
1480
+ ] }) : null,
1481
+ groupEntries.map((entry) => renderRow(entry)),
1482
+ groupPending.map((id) => renderPending(id, group.id)),
1483
+ renderComposer(group.id)
1484
+ ] }) : null
1485
+ ] }, group.id);
1486
+ }) }),
1291
1487
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1292
1488
  import_dsh_client_ui_primitives.Modal,
1293
1489
  {
@@ -1318,6 +1514,60 @@ function EnvSettingsSection({
1318
1514
  )
1319
1515
  ] })
1320
1516
  }
1517
+ ),
1518
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1519
+ import_dsh_client_ui_primitives.Modal,
1520
+ {
1521
+ open: groupModal !== void 0,
1522
+ onClose: () => {
1523
+ if (!busy) setGroupModal(void 0);
1524
+ },
1525
+ title: groupModal?.kind === "create" ? t("action.createGroup") : groupModal?.kind === "rename" ? t("action.renameGroup") : t("action.deleteGroupTitle"),
1526
+ closeLabel: t("action.close"),
1527
+ description: groupModal?.kind === "delete" ? t("action.deleteGroupConfirm", { title: groupModal.title }) : void 0,
1528
+ footer: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1529
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "outline", disabled: busy, onClick: () => {
1530
+ setGroupModal(void 0);
1531
+ }, children: t("action.cancel") }),
1532
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1533
+ import_dsh_client_ui_primitives.Button,
1534
+ {
1535
+ variant: "primary",
1536
+ disabled: busy || (groupModal?.kind === "create" || groupModal?.kind === "rename") && groupTitleDraft.trim() === "",
1537
+ onClick: () => {
1538
+ submitGroupModal();
1539
+ },
1540
+ children: groupModal?.kind === "delete" ? t("action.deleteGroup") : t("action.confirm")
1541
+ }
1542
+ )
1543
+ ] }),
1544
+ children: [
1545
+ groupModal?.kind === "create" || groupModal?.kind === "rename" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: EnvSettingsSection_default.modalField, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1546
+ import_dsh_client_ui_primitives.Input,
1547
+ {
1548
+ value: groupTitleDraft,
1549
+ placeholder: t("action.groupTitlePlaceholder"),
1550
+ "aria-label": t("action.groupTitle"),
1551
+ disabled: busy,
1552
+ onChange: (event) => {
1553
+ setGroupTitleDraft(event.target.value);
1554
+ }
1555
+ }
1556
+ ) }) : null,
1557
+ groupModal?.kind === "delete" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1558
+ import_dsh_client_ui_primitives.Checkbox,
1559
+ {
1560
+ className: EnvSettingsSection_default.modalCheck,
1561
+ checked: groupModal.moveToDefault,
1562
+ disabled: busy,
1563
+ label: t("action.deleteGroupMove"),
1564
+ onChange: (checked) => {
1565
+ setGroupModal({ ...groupModal, moveToDefault: checked });
1566
+ }
1567
+ }
1568
+ ) : null
1569
+ ]
1570
+ }
1321
1571
  )
1322
1572
  ] });
1323
1573
  }
@@ -1327,7 +1577,6 @@ var NS = "settings.env";
1327
1577
  var zh = {
1328
1578
  "panel.nav": "\u73AF\u5883\u53D8\u91CF",
1329
1579
  "panel.title": "\u73AF\u5883\u53D8\u91CF",
1330
- "panel.empty": "\u6682\u65E0\u53D8\u91CF",
1331
1580
  "panel.loading": "\u52A0\u8F7D\u4E2D\u2026",
1332
1581
  "field.name": "\u540D\u79F0",
1333
1582
  "field.value": "\u503C",
@@ -1341,6 +1590,16 @@ var zh = {
1341
1590
  "action.removeConfirm": "\u5220\u9664 {name}\uFF1F",
1342
1591
  "action.restore": "\u6062\u590D\u539F\u503C",
1343
1592
  "action.close": "\u5173\u95ED",
1593
+ "action.moveGroup": "\u4FEE\u6539\u5206\u7EC4",
1594
+ "action.createGroup": "\u65B0\u5EFA\u5206\u7EC4",
1595
+ "action.renameGroup": "\u91CD\u547D\u540D\u5206\u7EC4",
1596
+ "action.deleteGroup": "\u5220\u9664\u5206\u7EC4",
1597
+ "action.deleteGroupTitle": "\u5220\u9664\u5206\u7EC4",
1598
+ "action.deleteGroupConfirm": "\u5220\u9664\u5206\u7EC4\u300C{title}\u300D\uFF1F",
1599
+ "action.deleteGroupMove": "\u53D8\u91CF\u79FB\u5165\u9ED8\u8BA4\u5206\u7EC4",
1600
+ "action.groupTitle": "\u5206\u7EC4\u540D\u79F0",
1601
+ "action.groupTitlePlaceholder": "\u5206\u7EC4\u540D\u79F0",
1602
+ "group.defaultTitle": "\u9ED8\u8BA4\u5206\u7EC4",
1344
1603
  "status.restartRequired": "\u9700\u91CD\u542F\u5BA2\u6237\u7AEF",
1345
1604
  "cli.title": "dsh",
1346
1605
  "cli.activeMissing": "\u672A\u627E\u5230",
@@ -1349,6 +1608,7 @@ var zh = {
1349
1608
  "error.generic": "\u66F4\u65B0\u5931\u8D25",
1350
1609
  "error.invalidName": "\u540D\u79F0\u65E0\u6548",
1351
1610
  "error.invalidValue": "\u503C\u65E0\u6548",
1611
+ "error.invalidGroup": "\u5206\u7EC4\u65E0\u6548",
1352
1612
  "error.restoreFailed": "\u6062\u590D\u5931\u8D25",
1353
1613
  "error.cliInstall": "\u5B89\u88C5\u5931\u8D25",
1354
1614
  "error.cliUnresolved": "\u65E0\u6CD5\u5B89\u88C5",
@@ -1357,7 +1617,6 @@ var zh = {
1357
1617
  var en = {
1358
1618
  "panel.nav": "Environment",
1359
1619
  "panel.title": "Environment",
1360
- "panel.empty": "No variables",
1361
1620
  "panel.loading": "Loading\u2026",
1362
1621
  "field.name": "Name",
1363
1622
  "field.value": "Value",
@@ -1371,6 +1630,16 @@ var en = {
1371
1630
  "action.removeConfirm": "Delete {name}?",
1372
1631
  "action.restore": "Restore original",
1373
1632
  "action.close": "Close",
1633
+ "action.moveGroup": "Move to group",
1634
+ "action.createGroup": "New group",
1635
+ "action.renameGroup": "Rename group",
1636
+ "action.deleteGroup": "Delete group",
1637
+ "action.deleteGroupTitle": "Delete group",
1638
+ "action.deleteGroupConfirm": 'Delete group "{title}"?',
1639
+ "action.deleteGroupMove": "Move variables into Default",
1640
+ "action.groupTitle": "Group name",
1641
+ "action.groupTitlePlaceholder": "Group name",
1642
+ "group.defaultTitle": "Default",
1374
1643
  "status.restartRequired": "Restart the client",
1375
1644
  "cli.title": "dsh",
1376
1645
  "cli.activeMissing": "Not found",
@@ -1379,6 +1648,7 @@ var en = {
1379
1648
  "error.generic": "Update failed",
1380
1649
  "error.invalidName": "Invalid name",
1381
1650
  "error.invalidValue": "Invalid value",
1651
+ "error.invalidGroup": "Invalid group",
1382
1652
  "error.restoreFailed": "Restore failed",
1383
1653
  "error.cliInstall": "Install failed",
1384
1654
  "error.cliUnresolved": "Install unavailable",
@@ -1402,9 +1672,13 @@ function registerUi(ctx) {
1402
1672
  locale: NS,
1403
1673
  inject: () => ({
1404
1674
  load: async () => call(await ctx.remote.customEnv.list()),
1405
- upsert: async (id, name, value) => call(await ctx.remote.customEnv.upsert(id, name, value)),
1675
+ upsert: async (id, name, value, groupId) => call(await ctx.remote.customEnv.upsert(id, name, value, groupId)),
1406
1676
  remove: async (id) => call(await ctx.remote.customEnv.deleteEntry(id)),
1407
1677
  restore: async (id) => call(await ctx.remote.customEnv.restore(id)),
1678
+ moveEntry: async (id, groupId) => call(await ctx.remote.customEnv.moveEntry(id, groupId)),
1679
+ createGroup: async (title) => call(await ctx.remote.customEnv.createGroup(title)),
1680
+ renameGroup: async (id, title) => call(await ctx.remote.customEnv.renameGroup(id, title)),
1681
+ deleteGroup: async (id, moveToDefault) => call(await ctx.remote.customEnv.deleteGroup(id, { moveToDefault })),
1408
1682
  cliStatus: async () => call(await ctx.remote.customEnv.cliStatus()),
1409
1683
  cliInstall: async () => call(await ctx.remote.customEnv.cliInstall())
1410
1684
  })