@woosau/dsh-custom-env 0.1.6 → 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,26 +542,121 @@ 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
  }
553
549
  return t("error.generic");
554
550
  }
555
551
 
552
+ // src/client/parse-env-paste.ts
553
+ var NAME_OK = /^[A-Za-z_][A-Za-z0-9_]*$/u;
554
+ function parseEnvPaste(text) {
555
+ const lastByName = /* @__PURE__ */ new Map();
556
+ const order = [];
557
+ for (const rawLine of text.split(/\r\n|\n|\r/u)) {
558
+ const line = rawLine.trim();
559
+ if (line === "") continue;
560
+ const eq = line.indexOf("=");
561
+ if (eq <= 0) continue;
562
+ const name = line.slice(0, eq).trim();
563
+ const value = line.slice(eq + 1).trim();
564
+ if (!NAME_OK.test(name)) continue;
565
+ if (!lastByName.has(name)) order.push(name);
566
+ lastByName.set(name, value);
567
+ }
568
+ return order.map((name) => ({ name, value: lastByName.get(name) }));
569
+ }
570
+ function formatEnvCopy(pairs) {
571
+ const lines = [];
572
+ for (const pair of pairs) {
573
+ const name = pair.name.trim();
574
+ if (name === "") continue;
575
+ lines.push(`${name}=${pair.value}`);
576
+ }
577
+ return lines.join("\n");
578
+ }
579
+
580
+ // src/client/env-row-select.ts
581
+ function isSelectionEditKey(event) {
582
+ return event.ctrlKey || event.metaKey;
583
+ }
584
+ function rangeIds(ordered, from, to) {
585
+ const a = ordered.indexOf(from);
586
+ const b = ordered.indexOf(to);
587
+ if (a < 0 || b < 0) return from === to && a >= 0 ? [from] : [];
588
+ const start = Math.min(a, b);
589
+ const end = Math.max(a, b);
590
+ return ordered.slice(start, end + 1);
591
+ }
592
+ function clearDomTextSelection() {
593
+ const selection = window.getSelection();
594
+ if (selection !== null && selection.rangeCount > 0) selection.removeAllRanges();
595
+ }
596
+ function hasFocusedInputTextSelection() {
597
+ const active = document.activeElement;
598
+ if (!(active instanceof HTMLInputElement) && !(active instanceof HTMLTextAreaElement)) return false;
599
+ const start = active.selectionStart;
600
+ const end = active.selectionEnd;
601
+ return start !== null && end !== null && start !== end;
602
+ }
603
+ function selectedEnvCopyText(selectedIds, orderedIds, drafts) {
604
+ if (selectedIds.size === 0) return "";
605
+ const pairs = orderedIds.flatMap((id) => {
606
+ if (!selectedIds.has(id)) return [];
607
+ const draft = drafts[id];
608
+ if (draft === void 0) return [];
609
+ const name = draft.name.trim();
610
+ if (name === "") return [];
611
+ return [{ name, value: draft.value }];
612
+ });
613
+ return formatEnvCopy(pairs);
614
+ }
615
+ function rowIdFromPoint(clientX, clientY) {
616
+ const hit = document.elementFromPoint(clientX, clientY);
617
+ if (!(hit instanceof Element)) return void 0;
618
+ const row = hit.closest("[data-env-row]");
619
+ if (!(row instanceof HTMLElement)) return void 0;
620
+ const id = row.dataset.envRow;
621
+ return id === void 0 || id === "" ? void 0 : id;
622
+ }
623
+
556
624
  // src/client/EnvSettingsSection.module.css
557
- 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_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}\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";
558
626
  if (typeof document !== "undefined" && !document.getElementById("dsh-custom-env-css")) {
559
627
  const style = document.createElement("style");
560
628
  style.id = "dsh-custom-env-css";
561
629
  style.textContent = css;
562
630
  document.head.appendChild(style);
563
631
  }
564
- 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" };
565
633
 
566
634
  // src/client/EnvSettingsSection.tsx
567
635
  var import_jsx_runtime = require("react/jsx-runtime");
568
- var NAME_OK = /^[A-Za-z_][A-Za-z0-9_]*$/u;
569
- var NEW_ID = "";
636
+ var NAME_OK2 = /^[A-Za-z_][A-Za-z0-9_]*$/u;
637
+ var PENDING_PREFIX = "pending:";
638
+ var COMPOSER_PREFIX = "composer:";
639
+ function isPendingId(id) {
640
+ return id.startsWith(PENDING_PREFIX);
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
+ }
570
660
  function IconAction({
571
661
  label,
572
662
  disabled,
@@ -599,14 +689,37 @@ function RowActions({
599
689
  canConfirm,
600
690
  pending = false,
601
691
  showRemove = true,
692
+ moveItems,
602
693
  onConfirm,
603
694
  onCancel,
604
695
  onRestore,
605
- onRemove
696
+ onRemove,
697
+ onMove
606
698
  }) {
699
+ const [moveOpen, setMoveOpen] = (0, import_react.useState)(false);
700
+ const showMove = moveItems !== void 0 && moveItems.length > 0 && onMove !== void 0;
607
701
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.actions, role: "group", "aria-label": t("field.actions"), children: [
608
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 }) }) }) }),
609
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
+ ) }),
610
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 }) }) })
611
724
  ] });
612
725
  }
@@ -622,9 +735,17 @@ function Field(props) {
622
735
  title: props.title,
623
736
  "aria-invalid": props.invalid || void 0,
624
737
  "aria-label": props.label,
738
+ onMouseDown: (event) => {
739
+ if (!props.rowSelectGestures) {
740
+ event.stopPropagation();
741
+ return;
742
+ }
743
+ if (isSelectionEditKey(event)) event.preventDefault();
744
+ },
625
745
  onChange: (event) => {
626
746
  props.onChange(event.target.value);
627
- }
747
+ },
748
+ onPaste: props.onPaste
628
749
  }
629
750
  ) });
630
751
  }
@@ -634,25 +755,98 @@ function EnvSettingsSection({
634
755
  upsert,
635
756
  remove,
636
757
  restore,
758
+ moveEntry,
759
+ createGroup,
760
+ renameGroup,
761
+ deleteGroup,
637
762
  cliStatus,
638
763
  cliInstall
639
764
  }) {
640
765
  const [snapshot, setSnapshot] = (0, import_react.useState)();
641
766
  const [cli, setCli] = (0, import_react.useState)();
642
- const [drafts, setDrafts] = (0, import_react.useState)({ [NEW_ID]: { name: "", value: "" } });
767
+ const [drafts, setDrafts] = (0, import_react.useState)({});
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 });
643
771
  const [busyTarget, setBusyTarget] = (0, import_react.useState)();
644
772
  const [error, setError] = (0, import_react.useState)();
645
773
  const [nameError, setNameError] = (0, import_react.useState)(false);
646
774
  const [pendingRemove, setPendingRemove] = (0, import_react.useState)();
775
+ const [groupModal, setGroupModal] = (0, import_react.useState)();
776
+ const [groupTitleDraft, setGroupTitleDraft] = (0, import_react.useState)("");
777
+ const [selectedIds, setSelectedIds] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
778
+ const pendingSeq = (0, import_react.useRef)(0);
779
+ const draftsRef = (0, import_react.useRef)(drafts);
780
+ draftsRef.current = drafts;
781
+ const pendingOrderRef = (0, import_react.useRef)(pendingOrder);
782
+ pendingOrderRef.current = pendingOrder;
783
+ const pendingGroupRef = (0, import_react.useRef)(pendingGroup);
784
+ pendingGroupRef.current = pendingGroup;
785
+ const selectedRef = (0, import_react.useRef)(selectedIds);
786
+ selectedRef.current = selectedIds;
787
+ const anchorIdRef = (0, import_react.useRef)(void 0);
788
+ const dragModeRef = (0, import_react.useRef)(void 0);
789
+ const dragVisitedRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
790
+ const dragPendingCrossRef = (0, import_react.useRef)(false);
791
+ const pendingRemoveRef = (0, import_react.useRef)(pendingRemove);
792
+ pendingRemoveRef.current = pendingRemove;
793
+ const groupModalRef = (0, import_react.useRef)(groupModal);
794
+ groupModalRef.current = groupModal;
647
795
  const busy = busyTarget !== void 0;
796
+ const groups = snapshot?.groups ?? [{ id: DEFAULT_GROUP_ID, title: t("group.defaultTitle") }];
648
797
  const entries = snapshot?.entries ?? [];
649
798
  const taken = new Set(entries.map((entry) => entry.name));
650
- const applySnapshot = (next) => {
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
+ ]);
804
+ const selectableOrderRef = (0, import_react.useRef)(selectableOrder);
805
+ selectableOrderRef.current = selectableOrder;
806
+ const groupLabel = (group) => group.id === DEFAULT_GROUP_ID ? t("group.defaultTitle") : group.title;
807
+ const keepTransientDrafts = (current, dropId) => {
808
+ const kept = {};
809
+ for (const [id, draft] of Object.entries(current)) {
810
+ if (isComposerId(id)) kept[id] = draft;
811
+ else if (isPendingId(id) && id !== dropId) kept[id] = draft;
812
+ }
813
+ return kept;
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
+ };
835
+ const applySnapshot = (next, dropPendingId) => {
651
836
  setSnapshot(next);
652
- setDrafts({
653
- [NEW_ID]: { name: "", value: "" },
654
- ...Object.fromEntries(next.entries.map((entry) => [entry.id, { name: entry.name, value: entry.value }]))
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;
655
843
  });
844
+ if (dropPendingId !== void 0) forgetPendingId(dropPendingId);
845
+ setDrafts((current) => ({
846
+ ...emptyComposers(next.groups, current),
847
+ ...keepTransientDrafts(current, dropPendingId),
848
+ ...Object.fromEntries(next.entries.map((entry) => [entry.id, { name: entry.name, value: entry.value }]))
849
+ }));
656
850
  };
657
851
  const setDraft = (id, patch) => {
658
852
  setDrafts((previous) => ({
@@ -660,12 +854,39 @@ function EnvSettingsSection({
660
854
  [id]: { name: patch.name ?? previous[id]?.name ?? "", value: patch.value ?? previous[id]?.value ?? "" }
661
855
  }));
662
856
  };
663
- const run = async (target, action) => {
857
+ const dropPending = (id) => {
858
+ forgetPendingId(id);
859
+ setDrafts((previous) => {
860
+ const next = { ...previous };
861
+ delete next[id];
862
+ return next;
863
+ });
864
+ };
865
+ const clearSelection = () => {
866
+ setSelectedIds(/* @__PURE__ */ new Set());
867
+ anchorIdRef.current = void 0;
868
+ clearDomTextSelection();
869
+ };
870
+ const pruneSelection = (allowed) => {
871
+ setSelectedIds((previous) => {
872
+ let changed = false;
873
+ const next = /* @__PURE__ */ new Set();
874
+ for (const id of previous) {
875
+ if (allowed.has(id)) next.add(id);
876
+ else changed = true;
877
+ }
878
+ return changed ? next : previous;
879
+ });
880
+ if (anchorIdRef.current !== void 0 && !allowed.has(anchorIdRef.current)) {
881
+ anchorIdRef.current = void 0;
882
+ }
883
+ };
884
+ const run = async (target, action, dropPendingId) => {
664
885
  if (busyTarget !== void 0) return;
665
886
  setBusyTarget(target);
666
887
  setError(void 0);
667
888
  try {
668
- applySnapshot(await action());
889
+ applySnapshot(await action(), dropPendingId);
669
890
  } catch (err) {
670
891
  setError(formatRemoteError(err, t));
671
892
  } finally {
@@ -676,7 +897,16 @@ function EnvSettingsSection({
676
897
  setBusyTarget("boot");
677
898
  setError(void 0);
678
899
  try {
679
- applySnapshot(await load());
900
+ setPendingOrder([]);
901
+ setPendingGroup({});
902
+ clearSelection();
903
+ const next = await load();
904
+ setSnapshot(next);
905
+ setOpenGroups(Object.fromEntries(next.groups.map((group) => [group.id, group.id === DEFAULT_GROUP_ID])));
906
+ setDrafts({
907
+ ...emptyComposers(next.groups, {}),
908
+ ...Object.fromEntries(next.entries.map((entry) => [entry.id, { name: entry.name, value: entry.value }]))
909
+ });
680
910
  try {
681
911
  setCli(await cliStatus());
682
912
  } catch {
@@ -691,79 +921,277 @@ function EnvSettingsSection({
691
921
  (0, import_react.useEffect)(() => {
692
922
  void refresh();
693
923
  }, [refresh]);
694
- const saveRow = (id, entryName) => {
924
+ (0, import_react.useEffect)(() => {
925
+ pruneSelection(new Set(selectableOrder));
926
+ }, [selectableOrder.join("\0")]);
927
+ const stopEditingGesture = () => {
928
+ clearDomTextSelection();
929
+ const active = document.activeElement;
930
+ if (active instanceof HTMLInputElement && active.closest(`.${EnvSettingsSection_default.groups}`) !== null) active.blur();
931
+ };
932
+ const applyDragOverRow = (id) => {
933
+ if (dragModeRef.current === void 0) return;
934
+ if (dragModeRef.current === "toggle") {
935
+ if (dragVisitedRef.current.has(id)) return;
936
+ stopEditingGesture();
937
+ dragVisitedRef.current.add(id);
938
+ setSelectedIds((previous) => {
939
+ const next = new Set(previous);
940
+ if (next.has(id)) next.delete(id);
941
+ else next.add(id);
942
+ return next;
943
+ });
944
+ return;
945
+ }
946
+ if (dragPendingCrossRef.current && id === anchorIdRef.current) return;
947
+ if (dragPendingCrossRef.current || id !== anchorIdRef.current) {
948
+ dragPendingCrossRef.current = false;
949
+ stopEditingGesture();
950
+ }
951
+ dragVisitedRef.current.add(id);
952
+ setSelectedIds(new Set(rangeIds(selectableOrderRef.current, anchorIdRef.current ?? id, id)));
953
+ };
954
+ (0, import_react.useEffect)(() => {
955
+ if (selectedIds.size > 0) clearDomTextSelection();
956
+ }, [selectedIds]);
957
+ (0, import_react.useEffect)(() => {
958
+ const endDrag = () => {
959
+ dragModeRef.current = void 0;
960
+ dragVisitedRef.current = /* @__PURE__ */ new Set();
961
+ dragPendingCrossRef.current = false;
962
+ };
963
+ const onMove = (event) => {
964
+ if (event.buttons !== 1 || dragModeRef.current === void 0) return;
965
+ const id = rowIdFromPoint(event.clientX, event.clientY);
966
+ if (id !== void 0) applyDragOverRow(id);
967
+ };
968
+ const onPointerDown = (event) => {
969
+ if (selectedRef.current.size === 0 || pendingRemoveRef.current !== void 0 || groupModalRef.current !== void 0) return;
970
+ if (!(event.target instanceof Element)) return;
971
+ if (event.target.closest("[data-env-row]") !== null) return;
972
+ clearSelection();
973
+ };
974
+ const copyText = () => selectedEnvCopyText(selectedRef.current, selectableOrderRef.current, draftsRef.current);
975
+ const onKeyDown = (event) => {
976
+ if (selectedRef.current.size === 0) return;
977
+ if (event.key !== "c" && event.key !== "C" || !isSelectionEditKey(event) || event.altKey || event.shiftKey) return;
978
+ if (hasFocusedInputTextSelection()) return;
979
+ const text = copyText();
980
+ if (text === "") return;
981
+ event.preventDefault();
982
+ void navigator.clipboard.writeText(text);
983
+ };
984
+ const onCopy = (event) => {
985
+ if (selectedRef.current.size === 0 || hasFocusedInputTextSelection()) return;
986
+ const text = copyText();
987
+ if (text === "") return;
988
+ event.preventDefault();
989
+ event.clipboardData?.setData("text/plain", text);
990
+ };
991
+ window.addEventListener("mouseup", endDrag);
992
+ window.addEventListener("mousemove", onMove);
993
+ document.addEventListener("mousedown", onPointerDown, true);
994
+ document.addEventListener("keydown", onKeyDown, true);
995
+ document.addEventListener("copy", onCopy, true);
996
+ return () => {
997
+ window.removeEventListener("mouseup", endDrag);
998
+ window.removeEventListener("mousemove", onMove);
999
+ document.removeEventListener("mousedown", onPointerDown, true);
1000
+ document.removeEventListener("keydown", onKeyDown, true);
1001
+ document.removeEventListener("copy", onCopy, true);
1002
+ };
1003
+ }, []);
1004
+ const saveExisting = (id, entryName) => {
695
1005
  const draft = drafts[id] ?? { name: "", value: "" };
696
1006
  const name = draft.name.trim();
697
1007
  const takenByOther = name !== entryName && taken.has(name);
698
- if (!NAME_OK.test(name) || takenByOther) {
1008
+ if (!NAME_OK2.test(name) || takenByOther) {
1009
+ setNameError(true);
1010
+ return;
1011
+ }
1012
+ if (draft.value === "") return;
1013
+ setNameError(false);
1014
+ void run(`row:${id}`, () => upsert(id, name, draft.value));
1015
+ };
1016
+ const savePending = (id) => {
1017
+ const draft = drafts[id] ?? { name: "", value: "" };
1018
+ const name = draft.name.trim();
1019
+ if (!NAME_OK2.test(name) || taken.has(name)) {
699
1020
  setNameError(true);
700
1021
  return;
701
1022
  }
702
1023
  if (draft.value === "") return;
703
1024
  setNameError(false);
704
- const target = id === NEW_ID ? "new" : `row:${id}`;
705
- void run(target, () => upsert(id, name, draft.value));
1025
+ const groupId = pendingGroup[id] ?? DEFAULT_GROUP_ID;
1026
+ void run(`pending:${id}`, () => upsert("", name, draft.value, groupId), id);
1027
+ };
1028
+ const typeIntoComposer = (groupId, patch, focus) => {
1029
+ setNameError(false);
1030
+ const key = composerId(groupId);
1031
+ const base = draftsRef.current[key] ?? { name: "", value: "" };
1032
+ const next = { name: patch.name ?? base.name, value: patch.value ?? base.value };
1033
+ const id = `${PENDING_PREFIX}${++pendingSeq.current}`;
1034
+ setDrafts((previous) => ({
1035
+ ...previous,
1036
+ [id]: next,
1037
+ [key]: { name: "", value: "" }
1038
+ }));
1039
+ setPendingOrder((order) => [...order, id]);
1040
+ setPendingGroup((previous) => ({ ...previous, [id]: groupId }));
1041
+ queueMicrotask(() => {
1042
+ document.getElementById(`env-${focus}-${id}`)?.focus();
1043
+ });
1044
+ };
1045
+ const appendPendingPairs = (groupId, pairs) => {
1046
+ if (pairs.length === 0) return;
1047
+ setNameError(false);
1048
+ const previous = draftsRef.current;
1049
+ const order = pendingOrderRef.current;
1050
+ const groupsByPending = pendingGroupRef.current;
1051
+ const nextDrafts = { ...previous, [composerId(groupId)]: { name: "", value: "" } };
1052
+ const existingPendingByName = /* @__PURE__ */ new Map();
1053
+ for (const id of order) {
1054
+ const draft = previous[id];
1055
+ if (draft !== void 0) existingPendingByName.set(draft.name.trim(), id);
1056
+ }
1057
+ const savedByName = new Map(entries.map((entry) => [entry.name, entry.id]));
1058
+ const added = [];
1059
+ const dropPendingIds = /* @__PURE__ */ new Set();
1060
+ const nextPendingGroup = { ...groupsByPending };
1061
+ for (const pair of pairs) {
1062
+ const savedId = savedByName.get(pair.name);
1063
+ if (savedId !== void 0) {
1064
+ nextDrafts[savedId] = { name: pair.name, value: pair.value };
1065
+ const pendingDup = existingPendingByName.get(pair.name);
1066
+ if (pendingDup !== void 0) {
1067
+ delete nextDrafts[pendingDup];
1068
+ dropPendingIds.add(pendingDup);
1069
+ delete nextPendingGroup[pendingDup];
1070
+ existingPendingByName.delete(pair.name);
1071
+ }
1072
+ continue;
1073
+ }
1074
+ const reuse = existingPendingByName.get(pair.name);
1075
+ if (reuse !== void 0) {
1076
+ nextDrafts[reuse] = { name: pair.name, value: pair.value };
1077
+ continue;
1078
+ }
1079
+ const id = `${PENDING_PREFIX}${++pendingSeq.current}`;
1080
+ nextDrafts[id] = { name: pair.name, value: pair.value };
1081
+ existingPendingByName.set(pair.name, id);
1082
+ nextPendingGroup[id] = groupId;
1083
+ added.push(id);
1084
+ }
1085
+ setDrafts(nextDrafts);
1086
+ setPendingGroup(nextPendingGroup);
1087
+ if (added.length > 0 || dropPendingIds.size > 0) {
1088
+ setPendingOrder([...order.filter((id) => !dropPendingIds.has(id)), ...added]);
1089
+ }
1090
+ };
1091
+ const onComposerPaste = (groupId, event) => {
1092
+ const text = event.clipboardData.getData("text");
1093
+ const pairs = parseEnvPaste(text);
1094
+ if (pairs.length === 0) return;
1095
+ event.preventDefault();
1096
+ appendPendingPairs(groupId, pairs);
1097
+ };
1098
+ const onRowMouseDown = (id, event) => {
1099
+ if (event.button !== 0) return;
1100
+ const target = event.target;
1101
+ const onInput = target?.closest("input") !== null;
1102
+ const onControl = target?.closest('button, a, [role="button"]') !== null;
1103
+ if (isSelectionEditKey(event)) {
1104
+ event.preventDefault();
1105
+ clearDomTextSelection();
1106
+ dragPendingCrossRef.current = false;
1107
+ if (event.shiftKey && anchorIdRef.current !== void 0) {
1108
+ const spanned = rangeIds(selectableOrder, anchorIdRef.current, id);
1109
+ setSelectedIds(new Set(spanned));
1110
+ dragModeRef.current = "range";
1111
+ dragVisitedRef.current = new Set(spanned);
1112
+ return;
1113
+ }
1114
+ setSelectedIds((previous) => {
1115
+ const next = new Set(previous);
1116
+ if (next.has(id)) next.delete(id);
1117
+ else next.add(id);
1118
+ return next;
1119
+ });
1120
+ anchorIdRef.current = id;
1121
+ dragModeRef.current = "toggle";
1122
+ dragVisitedRef.current = /* @__PURE__ */ new Set([id]);
1123
+ return;
1124
+ }
1125
+ if (!onInput && !onControl) event.preventDefault();
1126
+ anchorIdRef.current = id;
1127
+ dragModeRef.current = "range";
1128
+ dragVisitedRef.current = /* @__PURE__ */ new Set([id]);
1129
+ dragPendingCrossRef.current = true;
1130
+ };
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
+ })();
706
1170
  };
707
- const newDraft = drafts[NEW_ID] ?? { name: "", value: "" };
708
- const newDirty = newDraft.name.trim() !== "" || newDraft.value !== "";
709
1171
  const cliInstalling = busyTarget === "cli";
710
1172
  const cliBooting = busyTarget === "boot" || cli === void 0;
711
1173
  const showCliAction = cli?.canInstall === true || cliInstalling;
712
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);
713
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.section, children: [
714
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: EnvSettingsSection_default.heading, children: t("panel.title") }),
715
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: EnvSettingsSection_default.cli, "aria-label": t("cli.title"), children: [
716
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: EnvSettingsSection_default.cliHeading, children: t("cli.title") }),
717
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.cliBody, children: [
718
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
719
- "p",
720
- {
721
- className: EnvSettingsSection_default.cliPath,
722
- title: cliBooting ? void 0 : cli.activePath ?? void 0,
723
- "aria-busy": cliBooting || cliInstalling,
724
- children: cliBooting ? t("panel.loading") : cli.activePath === null ? t("cli.activeMissing") : cli.activePath
725
- }
726
- ),
727
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: EnvSettingsSection_default.cliSlot, "data-empty": showCliAction ? "false" : "true", children: showCliAction ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
728
- IconAction,
729
- {
730
- label: cliInstalling ? t("panel.loading") : cli?.platform === "darwin" ? t("cli.macConflict") : t("cli.install"),
731
- disabled: busy,
732
- onClick: () => {
733
- if (busyTarget !== void 0) return;
734
- setBusyTarget("cli");
735
- setError(void 0);
736
- void (async () => {
737
- try {
738
- setCli(await cliInstall());
739
- } catch (err) {
740
- setError(formatRemoteError(err, t));
741
- } finally {
742
- setBusyTarget(void 0);
743
- }
744
- })();
745
- },
746
- children: cliInstalling ? /* @__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.IconLinkOutlineRegular, { size: 14 })
747
- }
748
- ) : null })
749
- ] })
750
- ] }),
751
- 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,
752
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.list, children: [
753
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.header, children: [
754
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.headerLabel, children: t("field.name") }),
755
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.headerLabel, children: t("field.value") }),
756
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: EnvSettingsSection_default.headerActions, "aria-hidden": "true" })
757
- ] }),
758
- snapshot !== void 0 && entries.length === 0 && notice === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: EnvSettingsSection_default.empty, children: t("panel.empty") }) : null,
759
- entries.map((entry) => {
760
- const draft = drafts[entry.id] ?? { name: entry.name, value: entry.value };
761
- const dirty = draft.name !== entry.name || draft.value !== entry.value;
762
- const pending = entry.kind === "added" || entry.kind === "modified";
763
- const loading = busyTarget === `row:${entry.id}`;
764
- const nextName = draft.name.trim();
765
- const canConfirm = NAME_OK.test(nextName) && draft.value !== "" && !(nextName !== entry.name && taken.has(nextName));
766
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.row, children: [
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: [
767
1195
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
768
1196
  Field,
769
1197
  {
@@ -773,6 +1201,7 @@ function EnvSettingsSection({
773
1201
  title: draft.name,
774
1202
  invalid: nameError && dirty,
775
1203
  label: t("field.name"),
1204
+ rowSelectGestures: true,
776
1205
  onChange: (name) => {
777
1206
  setNameError(false);
778
1207
  setDraft(entry.id, { name });
@@ -787,6 +1216,7 @@ function EnvSettingsSection({
787
1216
  disabled: busy,
788
1217
  title: draft.value,
789
1218
  label: t("field.value"),
1219
+ rowSelectGestures: true,
790
1220
  onChange: (value) => {
791
1221
  setDraft(entry.id, { value });
792
1222
  }
@@ -800,9 +1230,10 @@ function EnvSettingsSection({
800
1230
  loading,
801
1231
  dirty: dirty || loading,
802
1232
  canConfirm,
803
- pending,
1233
+ pending: pendingKind && !dirty ? true : false,
1234
+ moveItems: moveTargets(entry.groupId),
804
1235
  onConfirm: () => {
805
- saveRow(entry.id, entry.name);
1236
+ saveExisting(entry.id, entry.name);
806
1237
  },
807
1238
  onCancel: () => {
808
1239
  setNameError(false);
@@ -813,64 +1244,246 @@ function EnvSettingsSection({
813
1244
  },
814
1245
  onRemove: () => {
815
1246
  setPendingRemove({ id: entry.id, name: entry.name });
1247
+ },
1248
+ onMove: (groupId) => {
1249
+ void run(`row:${entry.id}`, () => moveEntry(entry.id, groupId));
816
1250
  }
817
1251
  }
818
1252
  )
819
- ] }, entry.id);
820
- }),
821
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.row, children: [
822
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
823
- Field,
824
- {
825
- id: "env-new-name",
826
- value: newDraft.name,
827
- disabled: busy,
828
- placeholder: t("field.namePlaceholder"),
829
- title: newDraft.name || t("field.namePlaceholder"),
830
- invalid: nameError,
831
- label: t("field.name"),
832
- onChange: (name) => {
833
- setNameError(false);
834
- setDraft(NEW_ID, { name });
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
+ }
835
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);
836
1348
  }
837
- ),
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
+ };
1371
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.section, children: [
1372
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: EnvSettingsSection_default.heading, children: t("panel.title") }),
1373
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: EnvSettingsSection_default.cli, "aria-label": t("cli.title"), children: [
1374
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: EnvSettingsSection_default.cliHeading, children: t("cli.title") }),
1375
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: EnvSettingsSection_default.cliBody, children: [
838
1376
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
839
- Field,
1377
+ "p",
840
1378
  {
841
- id: "env-new-value",
842
- value: newDraft.value,
843
- disabled: busy,
844
- placeholder: t("field.valuePlaceholder"),
845
- title: newDraft.value || t("field.valuePlaceholder"),
846
- label: t("field.value"),
847
- onChange: (value) => {
848
- setDraft(NEW_ID, { value });
849
- }
1379
+ className: EnvSettingsSection_default.cliPath,
1380
+ title: cliBooting ? void 0 : cli.activePath ?? void 0,
1381
+ "aria-busy": cliBooting || cliInstalling,
1382
+ children: cliBooting ? t("panel.loading") : cli.activePath === null ? t("cli.activeMissing") : cli.activePath
850
1383
  }
851
1384
  ),
852
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
853
- RowActions,
1385
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: EnvSettingsSection_default.cliSlot, "data-empty": showCliAction ? "false" : "true", children: showCliAction ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1386
+ IconAction,
854
1387
  {
855
- t,
856
- busy,
857
- loading: busyTarget === "new",
858
- dirty: newDirty || busyTarget === "new",
859
- canConfirm: NAME_OK.test(newDraft.name.trim()) && newDraft.value !== "" && !taken.has(newDraft.name.trim()),
860
- showRemove: false,
861
- onConfirm: () => {
862
- saveRow(NEW_ID, void 0);
863
- },
864
- onCancel: () => {
865
- setNameError(false);
866
- setDraft(NEW_ID, { name: "", value: "" });
1388
+ label: cliInstalling ? t("panel.loading") : cli?.platform === "darwin" ? t("cli.macConflict") : t("cli.install"),
1389
+ disabled: busy,
1390
+ onClick: () => {
1391
+ if (busyTarget !== void 0) return;
1392
+ setBusyTarget("cli");
1393
+ setError(void 0);
1394
+ void (async () => {
1395
+ try {
1396
+ setCli(await cliInstall());
1397
+ } catch (err) {
1398
+ setError(formatRemoteError(err, t));
1399
+ } finally {
1400
+ setBusyTarget(void 0);
1401
+ }
1402
+ })();
867
1403
  },
868
- onRestore: () => void 0,
869
- onRemove: () => void 0
1404
+ children: cliInstalling ? /* @__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.IconLinkOutlineRegular, { size: 14 })
870
1405
  }
871
- )
1406
+ ) : null })
872
1407
  ] })
873
1408
  ] }),
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,
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
+ }) }),
874
1487
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
875
1488
  import_dsh_client_ui_primitives.Modal,
876
1489
  {
@@ -901,6 +1514,60 @@ function EnvSettingsSection({
901
1514
  )
902
1515
  ] })
903
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
+ }
904
1571
  )
905
1572
  ] });
906
1573
  }
@@ -910,7 +1577,6 @@ var NS = "settings.env";
910
1577
  var zh = {
911
1578
  "panel.nav": "\u73AF\u5883\u53D8\u91CF",
912
1579
  "panel.title": "\u73AF\u5883\u53D8\u91CF",
913
- "panel.empty": "\u6682\u65E0\u53D8\u91CF",
914
1580
  "panel.loading": "\u52A0\u8F7D\u4E2D\u2026",
915
1581
  "field.name": "\u540D\u79F0",
916
1582
  "field.value": "\u503C",
@@ -924,6 +1590,16 @@ var zh = {
924
1590
  "action.removeConfirm": "\u5220\u9664 {name}\uFF1F",
925
1591
  "action.restore": "\u6062\u590D\u539F\u503C",
926
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",
927
1603
  "status.restartRequired": "\u9700\u91CD\u542F\u5BA2\u6237\u7AEF",
928
1604
  "cli.title": "dsh",
929
1605
  "cli.activeMissing": "\u672A\u627E\u5230",
@@ -932,6 +1608,7 @@ var zh = {
932
1608
  "error.generic": "\u66F4\u65B0\u5931\u8D25",
933
1609
  "error.invalidName": "\u540D\u79F0\u65E0\u6548",
934
1610
  "error.invalidValue": "\u503C\u65E0\u6548",
1611
+ "error.invalidGroup": "\u5206\u7EC4\u65E0\u6548",
935
1612
  "error.restoreFailed": "\u6062\u590D\u5931\u8D25",
936
1613
  "error.cliInstall": "\u5B89\u88C5\u5931\u8D25",
937
1614
  "error.cliUnresolved": "\u65E0\u6CD5\u5B89\u88C5",
@@ -940,7 +1617,6 @@ var zh = {
940
1617
  var en = {
941
1618
  "panel.nav": "Environment",
942
1619
  "panel.title": "Environment",
943
- "panel.empty": "No variables",
944
1620
  "panel.loading": "Loading\u2026",
945
1621
  "field.name": "Name",
946
1622
  "field.value": "Value",
@@ -954,6 +1630,16 @@ var en = {
954
1630
  "action.removeConfirm": "Delete {name}?",
955
1631
  "action.restore": "Restore original",
956
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",
957
1643
  "status.restartRequired": "Restart the client",
958
1644
  "cli.title": "dsh",
959
1645
  "cli.activeMissing": "Not found",
@@ -962,6 +1648,7 @@ var en = {
962
1648
  "error.generic": "Update failed",
963
1649
  "error.invalidName": "Invalid name",
964
1650
  "error.invalidValue": "Invalid value",
1651
+ "error.invalidGroup": "Invalid group",
965
1652
  "error.restoreFailed": "Restore failed",
966
1653
  "error.cliInstall": "Install failed",
967
1654
  "error.cliUnresolved": "Install unavailable",
@@ -985,9 +1672,13 @@ function registerUi(ctx) {
985
1672
  locale: NS,
986
1673
  inject: () => ({
987
1674
  load: async () => call(await ctx.remote.customEnv.list()),
988
- 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)),
989
1676
  remove: async (id) => call(await ctx.remote.customEnv.deleteEntry(id)),
990
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 })),
991
1682
  cliStatus: async () => call(await ctx.remote.customEnv.cliStatus()),
992
1683
  cliInstall: async () => call(await ctx.remote.customEnv.cliInstall())
993
1684
  })