@agimon-ai/po-template-advanced 0.0.0-alpha.21 → 0.0.0-alpha.23

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.
@@ -3,10 +3,23 @@ import { View } from "react-native-unistyles/components/native/View";
3
3
  import { Text } from "react-native-unistyles/components/native/Text";
4
4
  import { ScrollView } from "react-native-unistyles/components/native/ScrollView";
5
5
  import { Pressable } from "react-native-unistyles/components/native/Pressable";
6
- import { ActivityIcon, AlertIcon, Badge, BranchIcon, Button, EmptyState, FolderIcon, ForkIcon, Input, KebabIcon, Label, ListRow, PlusIcon, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, Spinner, UsersIcon, fontOf } from "@agimon-ai/po-native-components";
6
+ import { ActivityIcon, AlertIcon, Badge, BranchIcon, Button, CheckIcon, EditIcon, EmptyState, FolderIcon, ForkIcon, Input, KebabIcon, Label, ListRow, PlusIcon, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, Spinner, TrashIcon, UsersIcon, fontOf } from "@agimon-ai/po-native-components";
7
7
  import { useEffect, useState } from "react";
8
8
  import { StyleSheet } from "react-native-unistyles";
9
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
+ /** Every group some session uses, in rail order, with the other sessions in it. */
11
+ function sessionGroupOptions(sessions, editedId, extra = []) {
12
+ return [.../* @__PURE__ */ new Set([...sessions.flatMap((session) => session.groups ?? []), ...extra])].map((name) => ({
13
+ name,
14
+ members: sessions.filter((session) => session.id !== editedId && session.groups?.includes(name) === true).map((session) => session.name || "Untitled")
15
+ }));
16
+ }
17
+ /** Why a typed name cannot become a group, or undefined when it can. */
18
+ function createProblem(name, count) {
19
+ if (name.length > 32) return `group names are at most ${String(32)} characters.`;
20
+ if (count >= 8) return `a session can belong to at most ${String(8)} groups.`;
21
+ }
22
+ //#endregion
10
23
  //#region src/extensions/(frontend)/template/_components/AdvancedSessionMenus.native.tsx
11
24
  function Items({ items }) {
12
25
  return /* @__PURE__ */ jsx(View, {
@@ -31,8 +44,8 @@ function Items({ items }) {
31
44
  }, item.label))
32
45
  });
33
46
  }
34
- /** A session card's ⋮: the cockpit's rename, resume, restart and remove, with remove confirmed first. */
35
- function SessionMenuSheet({ session, actions, onClose }) {
47
+ /** A session card's ⋮: the cockpit's rename, groups, resume, restart and remove, with remove confirmed first. */
48
+ function SessionMenuSheet({ session, actions, onGroups, onClose }) {
36
49
  const [step, setStep] = useState("menu");
37
50
  const [draft, setDraft] = useState("");
38
51
  const [busy, setBusy] = useState(false);
@@ -125,6 +138,14 @@ function SessionMenuSheet({ session, actions, onClose }) {
125
138
  setStep("rename");
126
139
  }
127
140
  },
141
+ ...onGroups ? [{
142
+ label: "Groups",
143
+ hint: "Sessions in a group can message each other",
144
+ onPress: () => {
145
+ close();
146
+ onGroups(session);
147
+ }
148
+ }] : [],
128
149
  {
129
150
  label: "Resume",
130
151
  hint: "Open an earlier conversation in this session",
@@ -239,6 +260,317 @@ function WorkspaceMenuSheet({ workspace, actions, onClose }) {
239
260
  })] }) })
240
261
  });
241
262
  }
263
+ /**
264
+ * The cockpit's groups dialog as a sheet. Rows check groups on and off and the typed name can
265
+ * become a new one; rename and delete act on every session in the group at once, the checks wait for Save.
266
+ */
267
+ function SessionGroupsSheet({ session, sessions, actions, onClose }) {
268
+ const [forId, setForId] = useState(null);
269
+ const [value, setValue] = useState([]);
270
+ const [query, setQuery] = useState("");
271
+ const [renaming, setRenaming] = useState();
272
+ const [deleting, setDeleting] = useState();
273
+ const [error, setError] = useState("");
274
+ const [busy, setBusy] = useState(false);
275
+ if ((session?.id ?? null) !== forId) {
276
+ setForId(session?.id ?? null);
277
+ setValue([...session?.groups ?? []]);
278
+ setQuery("");
279
+ setRenaming(void 0);
280
+ setDeleting(void 0);
281
+ setError("");
282
+ }
283
+ const close = () => {
284
+ if (!busy) onClose();
285
+ };
286
+ const rowCancel = () => {
287
+ setRenaming(void 0);
288
+ setDeleting(void 0);
289
+ };
290
+ const run = async (work, done) => {
291
+ setBusy(true);
292
+ setError("");
293
+ const result = await work();
294
+ setBusy(false);
295
+ if (result === void 0) done();
296
+ else setError(result.error);
297
+ };
298
+ const typed = query.trim();
299
+ const options = session === null ? [] : sessionGroupOptions(sessions, session.id, value);
300
+ const shown = options.filter((group) => group.name.includes(typed));
301
+ const canCreate = typed !== "" && !options.some((group) => group.name === typed);
302
+ const problem = canCreate ? createProblem(typed, value.length) : void 0;
303
+ const toggle = (group) => {
304
+ setError("");
305
+ if (value.includes(group)) setValue(value.filter((name) => name !== group));
306
+ else if (value.length >= 8) setError(createProblem("", value.length) ?? "");
307
+ else setValue([...value, group]);
308
+ };
309
+ const create = () => {
310
+ if (!canCreate || problem !== void 0) return;
311
+ setValue([...value, typed]);
312
+ setQuery("");
313
+ };
314
+ const renameConfirm = () => {
315
+ if (renaming === void 0) return;
316
+ const to = renaming.draft.trim();
317
+ if (to === "" || to === renaming.group) return rowCancel();
318
+ run(() => actions.renameGroup(renaming.group, to), () => {
319
+ setValue((current) => [...new Set(current.map((name) => name === renaming.group ? to : name))]);
320
+ rowCancel();
321
+ });
322
+ };
323
+ const deleteConfirm = () => {
324
+ if (deleting === void 0) return;
325
+ run(() => actions.deleteGroup(deleting), () => {
326
+ setValue((current) => current.filter((name) => name !== deleting));
327
+ rowCancel();
328
+ });
329
+ };
330
+ return /* @__PURE__ */ jsx(Sheet, {
331
+ open: session !== null,
332
+ onOpenChange: (open) => !open && close(),
333
+ children: /* @__PURE__ */ jsx(SheetContent, { children: session === null ? null : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: "Session groups" }) }), /* @__PURE__ */ jsxs(SheetBody, {
334
+ style: styles$1.body,
335
+ children: [
336
+ /* @__PURE__ */ jsx(Text, {
337
+ style: styles$1.meta,
338
+ children: "Sessions that share a group can message each other."
339
+ }),
340
+ /* @__PURE__ */ jsx(Input, {
341
+ value: query,
342
+ onChangeText: setQuery,
343
+ placeholder: value.length === 0 ? "Find or create a group" : "Add another",
344
+ autoCapitalize: "none",
345
+ autoCorrect: false,
346
+ returnKeyType: "done",
347
+ onSubmitEditing: () => {
348
+ if (canCreate) create();
349
+ else if (shown[0] !== void 0) {
350
+ toggle(shown[0].name);
351
+ setQuery("");
352
+ }
353
+ },
354
+ accessibilityLabel: "Find or create a group"
355
+ }),
356
+ shown.length > 0 || canCreate ? /* @__PURE__ */ jsxs(View, {
357
+ style: styles$1.list,
358
+ children: [shown.map((group, index) => {
359
+ const on = value.includes(group.name);
360
+ const total = group.members.length + (on ? 1 : 0);
361
+ const reach = total === 1 ? "its one session" : `all ${String(total)} sessions`;
362
+ const edge = index > 0 && styles$1.divider;
363
+ if (renaming?.group === group.name) return /* @__PURE__ */ jsxs(View, {
364
+ style: [styles$1.rowEdit, edge],
365
+ children: [
366
+ /* @__PURE__ */ jsx(Input, {
367
+ value: renaming.draft,
368
+ onChangeText: (draft) => setRenaming({
369
+ ...renaming,
370
+ draft
371
+ }),
372
+ autoFocus: true,
373
+ autoCapitalize: "none",
374
+ autoCorrect: false,
375
+ onSubmitEditing: renameConfirm,
376
+ accessibilityLabel: `Rename ${group.name}`
377
+ }),
378
+ /* @__PURE__ */ jsxs(Text, {
379
+ style: styles$1.meta,
380
+ children: [
381
+ "Renames it on ",
382
+ reach,
383
+ "."
384
+ ]
385
+ }),
386
+ /* @__PURE__ */ jsxs(View, {
387
+ style: styles$1.rowActions,
388
+ children: [/* @__PURE__ */ jsx(Button, {
389
+ variant: "ghost",
390
+ size: "sm",
391
+ disabled: busy,
392
+ onPress: rowCancel,
393
+ children: "Cancel"
394
+ }), /* @__PURE__ */ jsx(Button, {
395
+ variant: "outline",
396
+ size: "sm",
397
+ disabled: renaming.draft.trim() === "",
398
+ loading: busy,
399
+ loadingLabel: "Renaming",
400
+ onPress: renameConfirm,
401
+ children: "Rename"
402
+ })]
403
+ })
404
+ ]
405
+ }, group.name);
406
+ if (deleting === group.name) return /* @__PURE__ */ jsxs(View, {
407
+ style: [styles$1.rowEdit, edge],
408
+ children: [/* @__PURE__ */ jsxs(Text, {
409
+ style: styles$1.text,
410
+ children: [
411
+ "Remove ",
412
+ /* @__PURE__ */ jsx(Text, {
413
+ style: styles$1.strong,
414
+ children: group.name
415
+ }),
416
+ " from ",
417
+ reach,
418
+ "? They can no longer message through it."
419
+ ]
420
+ }), /* @__PURE__ */ jsxs(View, {
421
+ style: styles$1.rowActions,
422
+ children: [/* @__PURE__ */ jsx(Button, {
423
+ variant: "ghost",
424
+ size: "sm",
425
+ disabled: busy,
426
+ onPress: rowCancel,
427
+ children: "Cancel"
428
+ }), /* @__PURE__ */ jsx(Button, {
429
+ variant: "danger",
430
+ size: "sm",
431
+ loading: busy,
432
+ loadingLabel: "Deleting",
433
+ onPress: deleteConfirm,
434
+ children: "Delete"
435
+ })]
436
+ })]
437
+ }, group.name);
438
+ return /* @__PURE__ */ jsx(ListRow, {
439
+ title: group.name,
440
+ description: group.members.length === 0 ? "No other sessions yet" : group.members.join(", "),
441
+ selected: on,
442
+ leading: /* @__PURE__ */ jsx(View, {
443
+ style: styles$1.check,
444
+ children: on ? /* @__PURE__ */ jsx(CheckIcon, {
445
+ size: 16,
446
+ color: "green"
447
+ }) : null
448
+ }),
449
+ trailing: /* @__PURE__ */ jsxs(View, {
450
+ style: styles$1.rowActions,
451
+ children: [/* @__PURE__ */ jsx(Button, {
452
+ variant: "ghost",
453
+ size: "icon",
454
+ accessibilityLabel: `Rename ${group.name}`,
455
+ onPress: () => {
456
+ setDeleting(void 0);
457
+ setRenaming({
458
+ group: group.name,
459
+ draft: group.name
460
+ });
461
+ },
462
+ children: /* @__PURE__ */ jsx(EditIcon, {
463
+ size: 16,
464
+ color: "dim"
465
+ })
466
+ }), /* @__PURE__ */ jsx(Button, {
467
+ variant: "ghost",
468
+ size: "icon",
469
+ accessibilityLabel: `Delete ${group.name}`,
470
+ onPress: () => {
471
+ setRenaming(void 0);
472
+ setDeleting(group.name);
473
+ },
474
+ children: /* @__PURE__ */ jsx(TrashIcon, {
475
+ size: 16,
476
+ color: "dim"
477
+ })
478
+ })]
479
+ }),
480
+ disclosure: false,
481
+ onPress: () => toggle(group.name),
482
+ style: edge
483
+ }, group.name);
484
+ }), canCreate ? /* @__PURE__ */ jsx(ListRow, {
485
+ title: /* @__PURE__ */ jsxs(Text, {
486
+ style: styles$1.create(problem === void 0),
487
+ numberOfLines: 1,
488
+ children: [
489
+ "Create group “",
490
+ typed,
491
+ "”"
492
+ ]
493
+ }),
494
+ description: problem === void 0 ? void 0 : /* @__PURE__ */ jsx(Text, {
495
+ style: styles$1.error,
496
+ children: problem
497
+ }),
498
+ leading: /* @__PURE__ */ jsx(PlusIcon, {
499
+ size: 16,
500
+ color: problem === void 0 ? "magenta" : "faint"
501
+ }),
502
+ disabled: problem !== void 0,
503
+ disclosure: false,
504
+ onPress: create,
505
+ style: shown.length > 0 && styles$1.divider
506
+ }) : null]
507
+ }) : null,
508
+ /* @__PURE__ */ jsxs(Text, {
509
+ style: styles$1.meta,
510
+ children: [
511
+ value.length,
512
+ " of ",
513
+ 8,
514
+ " groups"
515
+ ]
516
+ }),
517
+ error ? /* @__PURE__ */ jsx(Text, {
518
+ style: styles$1.error,
519
+ accessibilityRole: "alert",
520
+ children: error
521
+ }) : null,
522
+ /* @__PURE__ */ jsx(Button, {
523
+ variant: "primary",
524
+ size: "lg",
525
+ loading: busy && renaming === void 0 && deleting === void 0,
526
+ loadingLabel: "Saving",
527
+ disabled: busy,
528
+ onPress: () => void run(() => actions.setSessionGroups(session.id, value), onClose),
529
+ children: "Save"
530
+ })
531
+ ]
532
+ })] }) })
533
+ });
534
+ }
535
+ /** A pending setup's ⋮: it has no runtime, so removing it is the only action, as in the cockpit. */
536
+ function SetupMenuSheet({ target, removePendingSetup, onClose }) {
537
+ const [busy, setBusy] = useState(false);
538
+ const [error, setError] = useState("");
539
+ const close = () => {
540
+ if (busy) return;
541
+ setError("");
542
+ onClose();
543
+ };
544
+ const remove = async () => {
545
+ if (target === null) return;
546
+ setBusy(true);
547
+ setError("");
548
+ const result = await removePendingSetup(target.sessionId, target.setup.id);
549
+ setBusy(false);
550
+ if (result === void 0) onClose();
551
+ else setError(result.error);
552
+ };
553
+ return /* @__PURE__ */ jsx(Sheet, {
554
+ open: target !== null,
555
+ onOpenChange: (open) => !open && close(),
556
+ children: /* @__PURE__ */ jsx(SheetContent, { children: target === null ? null : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, {
557
+ numberOfLines: 1,
558
+ children: target.setup.name
559
+ }) }), /* @__PURE__ */ jsxs(SheetBody, {
560
+ style: styles$1.body,
561
+ children: [/* @__PURE__ */ jsx(Items, { items: [{
562
+ label: busy ? "Removing…" : "Remove setup",
563
+ danger: true,
564
+ disabled: busy,
565
+ onPress: () => void remove()
566
+ }] }), error ? /* @__PURE__ */ jsx(Text, {
567
+ style: styles$1.error,
568
+ accessibilityRole: "alert",
569
+ children: error
570
+ }) : null]
571
+ })] }) })
572
+ });
573
+ }
242
574
  const SUGGESTION_DEBOUNCE_MS = 120;
243
575
  const TRAILING_SEPARATORS = /\/+$/u;
244
576
  function folderName(path) {
@@ -521,6 +853,24 @@ const styles$1 = StyleSheet.create((theme) => {
521
853
  paddingBottom: theme.spacing(2),
522
854
  uni__dependencies: [0]
523
855
  },
856
+ rowEdit: {
857
+ gap: theme.spacing(2),
858
+ paddingHorizontal: theme.spacing(4),
859
+ paddingVertical: theme.spacing(3),
860
+ uni__dependencies: [0]
861
+ },
862
+ rowActions: {
863
+ flexDirection: "row",
864
+ alignItems: "center",
865
+ justifyContent: "flex-end",
866
+ gap: theme.spacing(1),
867
+ uni__dependencies: [0]
868
+ },
869
+ check: {
870
+ width: 16,
871
+ alignItems: "center"
872
+ },
873
+ create: (enabled) => text("base", enabled ? theme.colors.magenta : theme.colors.faint),
524
874
  pressed: {
525
875
  backgroundColor: theme.colors["tint-green"],
526
876
  uni__dependencies: [0]
@@ -572,7 +922,7 @@ function IconButton({ label, onPress, children }) {
572
922
  children
573
923
  });
574
924
  }
575
- function SessionCard({ session, onOpen, onMenu }) {
925
+ function SessionCard({ session, onOpen, onMenu, onSetupMenu }) {
576
926
  const tone = statusTone(session.status);
577
927
  const name = session.name || "Untitled";
578
928
  const markColor = session.active ? "green" : "faint";
@@ -690,10 +1040,20 @@ function SessionCard({ session, onOpen, onMenu }) {
690
1040
  }), session.pendingSetups.map((setup) => /* @__PURE__ */ jsxs(View, {
691
1041
  style: styles.setup,
692
1042
  children: [
693
- /* @__PURE__ */ jsx(Text, {
694
- style: styles.name,
695
- numberOfLines: 1,
696
- children: setup.name
1043
+ /* @__PURE__ */ jsxs(View, {
1044
+ style: styles.setupHead,
1045
+ children: [/* @__PURE__ */ jsx(Text, {
1046
+ style: [styles.name, styles.setupName],
1047
+ numberOfLines: 1,
1048
+ children: setup.name
1049
+ }), onSetupMenu ? /* @__PURE__ */ jsx(IconButton, {
1050
+ label: `${setup.name} setup actions`,
1051
+ onPress: () => onSetupMenu(setup),
1052
+ children: /* @__PURE__ */ jsx(KebabIcon, {
1053
+ size: 16,
1054
+ color: "dim"
1055
+ })
1056
+ }) : null]
697
1057
  }),
698
1058
  /* @__PURE__ */ jsx(Text, {
699
1059
  style: styles.setupStatus(setup.failed),
@@ -707,7 +1067,7 @@ function SessionCard({ session, onOpen, onMenu }) {
707
1067
  ]
708
1068
  }, setup.id))] });
709
1069
  }
710
- function WorkspaceGroup({ workspace, onOpen, actions, onSessionMenu, onWorkspaceMenu }) {
1070
+ function WorkspaceGroup({ workspace, onOpen, actions, onSessionMenu, onSetupMenu, onWorkspaceMenu }) {
711
1071
  return /* @__PURE__ */ jsxs(View, {
712
1072
  style: styles.group,
713
1073
  children: [
@@ -759,7 +1119,8 @@ function WorkspaceGroup({ workspace, onOpen, actions, onSessionMenu, onWorkspace
759
1119
  }) : workspace.sessions.map((session) => /* @__PURE__ */ jsx(SessionCard, {
760
1120
  session,
761
1121
  onOpen,
762
- onMenu: onSessionMenu
1122
+ onMenu: onSessionMenu,
1123
+ ...onSetupMenu ? { onSetupMenu: (setup) => onSetupMenu(session.id, setup) } : {}
763
1124
  }, session.id))
764
1125
  ]
765
1126
  });
@@ -793,6 +1154,8 @@ function NoWorkspaces({ actions }) {
793
1154
  function AdvancedSessions({ workspaces, openSession, actions }) {
794
1155
  const [sessionMenu, setSessionMenu] = useState(null);
795
1156
  const [workspaceMenu, setWorkspaceMenu] = useState(null);
1157
+ const [groupsFor, setGroupsFor] = useState(null);
1158
+ const [setupMenu, setSetupMenu] = useState(null);
796
1159
  if (workspaces.length === 0) {
797
1160
  const { addWorkspace, searchDirectories, listDirectory } = actions;
798
1161
  return addWorkspace && searchDirectories && listDirectory ? /* @__PURE__ */ jsx(NoWorkspaces, { actions: {
@@ -808,6 +1171,12 @@ function AdvancedSessions({ workspaces, openSession, actions }) {
808
1171
  });
809
1172
  }
810
1173
  const sessions = workspaces.flatMap((workspace) => workspace.sessions);
1174
+ const { setSessionGroups, renameGroup, deleteGroup, removePendingSetup } = actions;
1175
+ const groupActions = setSessionGroups && renameGroup && deleteGroup ? {
1176
+ setSessionGroups,
1177
+ renameGroup,
1178
+ deleteGroup
1179
+ } : void 0;
811
1180
  return /* @__PURE__ */ jsxs(Fragment, { children: [
812
1181
  /* @__PURE__ */ jsx(ScrollView, {
813
1182
  style: styles.screen,
@@ -818,19 +1187,35 @@ function AdvancedSessions({ workspaces, openSession, actions }) {
818
1187
  onOpen: openSession,
819
1188
  actions,
820
1189
  onSessionMenu: (session) => setSessionMenu(session.id),
1190
+ ...removePendingSetup ? { onSetupMenu: (sessionId, setup) => setSetupMenu({
1191
+ sessionId,
1192
+ setup
1193
+ }) } : {},
821
1194
  onWorkspaceMenu: (entry) => setWorkspaceMenu(entry.id)
822
1195
  }, workspace.id))
823
1196
  }),
824
1197
  /* @__PURE__ */ jsx(SessionMenuSheet, {
825
1198
  session: sessions.find((session) => session.id === sessionMenu) ?? null,
826
1199
  actions,
1200
+ ...groupActions ? { onGroups: (session) => setGroupsFor(session.id) } : {},
827
1201
  onClose: () => setSessionMenu(null)
828
1202
  }),
829
1203
  /* @__PURE__ */ jsx(WorkspaceMenuSheet, {
830
1204
  workspace: workspaces.find((workspace) => workspace.id === workspaceMenu) ?? null,
831
1205
  actions,
832
1206
  onClose: () => setWorkspaceMenu(null)
833
- })
1207
+ }),
1208
+ groupActions ? /* @__PURE__ */ jsx(SessionGroupsSheet, {
1209
+ session: sessions.find((session) => session.id === groupsFor) ?? null,
1210
+ sessions,
1211
+ actions: groupActions,
1212
+ onClose: () => setGroupsFor(null)
1213
+ }) : null,
1214
+ removePendingSetup ? /* @__PURE__ */ jsx(SetupMenuSheet, {
1215
+ target: setupMenu,
1216
+ removePendingSetup,
1217
+ onClose: () => setSetupMenu(null)
1218
+ }) : null
834
1219
  ] });
835
1220
  }
836
1221
  const styles = StyleSheet.create((theme) => {
@@ -1027,6 +1412,14 @@ const styles = StyleSheet.create((theme) => {
1027
1412
  uni__dependencies: [0]
1028
1413
  },
1029
1414
  setupStatus: (failed) => text("sm", failed ? theme.colors.red : theme.colors.dim),
1415
+ setupHead: {
1416
+ flexDirection: "row",
1417
+ alignItems: "center",
1418
+ gap: theme.spacing(2),
1419
+ marginVertical: -theme.spacing(2),
1420
+ uni__dependencies: [0]
1421
+ },
1422
+ setupName: { flex: 1 },
1030
1423
  recovery: text("sm", theme.colors.dim),
1031
1424
  error: text("sm", theme.colors.red)
1032
1425
  };