@monster-orange/den 0.4.11 → 0.4.13

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/README.md +7 -7
  3. package/docs/distributions.md +212 -12
  4. package/package.json +2 -1
  5. package/src/shared/agents/catalog.ts +77 -0
  6. package/src/shared/agents/default-pack/be-dev.md +1 -0
  7. package/src/shared/agents/default-pack/devops.md +1 -0
  8. package/src/shared/agents/default-pack/fe-dev.md +1 -0
  9. package/src/shared/agents/default-pack/orchestrator.md +3 -1
  10. package/src/shared/agents/default-pack/planner.md +1 -0
  11. package/src/shared/agents/default-pack/reviewer.md +1 -0
  12. package/src/shared/agents/default-pack/tester.md +16 -0
  13. package/src/shared/agents/default-pack/ux-designer.md +1 -0
  14. package/src/shared/agents/den-claude.ts +151 -17
  15. package/src/shared/agents/permissions.ts +108 -0
  16. package/src/shared/agents/persona-store.ts +286 -0
  17. package/src/shared/agents/personas.ts +116 -12
  18. package/src/shared/agents/records.ts +57 -0
  19. package/src/shared/agents/roles.ts +288 -0
  20. package/src/shared/agents/runtime.ts +49 -0
  21. package/src/shared/agents/session-name.ts +110 -0
  22. package/src/shared/agents/transcript-follow.ts +180 -0
  23. package/src/shared/den-config.ts +27 -1
  24. package/src/ui/cli/agent-ops.ts +578 -11
  25. package/src/ui/cli/agents-command.ts +12 -2
  26. package/src/ui/cli/env-probe.ts +4 -1
  27. package/src/ui/mcp/tools.ts +1 -1
  28. package/src/ui/server/agent-paths.ts +52 -0
  29. package/src/ui/server/agent-routes.ts +329 -19
  30. package/src/ui/server/agents-layout-pref.ts +43 -0
  31. package/src/ui/server/editor-open-pref.ts +41 -0
  32. package/src/ui/server/index.ts +22 -0
  33. package/src/ui/server/public/agent-start.js +3 -2
  34. package/src/ui/server/public/agents-page.js +1037 -67
  35. package/src/ui/server/public/agents-section.js +1 -1
  36. package/src/ui/server/public/agents.html +236 -20
  37. package/src/ui/server/public/header.js +2 -0
  38. package/src/ui/server/public/i18n/es.js +139 -0
  39. package/src/ui/server/public/i18n/ja.js +139 -0
  40. package/src/ui/server/public/persona-ui.css +539 -0
  41. package/src/ui/server/public/persona-ui.js +796 -0
  42. package/src/ui/server/public/settings-page.js +4 -9
  43. package/src/ui/server/public/settings-registry.js +66 -0
  44. package/src/ui/server/public/settings.html +1 -0
  45. package/src/ui/server/public/shell.js +137 -0
  46. package/src/ui/server/public/term-input.js +129 -0
  47. package/src/ui/server/public/topbar.js +22 -4
  48. package/src/ui/server/route-classes.ts +29 -0
  49. package/src/ui/server/session-routes.ts +34 -1
  50. package/src/ui/server/ui-assets.ts +8 -0
@@ -148,15 +148,10 @@ export async function mountSettings(deps) {
148
148
  });
149
149
  if (!r.ok) return false;
150
150
  const reply = await r.json().catch(() => ({}));
151
- for (const k of [
152
- "editor",
153
- "terminal",
154
- "editor_overrides",
155
- "graph",
156
- "appearance",
157
- "shell",
158
- ])
159
- if (reply[k] !== undefined) ctx.data[k] = reply[k];
151
+ // Every setting the reply returns (0.4.13): a fixed list here left a
152
+ // new row's pressed button on its old value until a reload.
153
+ for (const [k, v] of Object.entries(reply))
154
+ if (k !== "ok" && v !== undefined) ctx.data[k] = v;
160
155
  return true;
161
156
  }
162
157
  async function saveWorkspace(patch) {
@@ -29,6 +29,7 @@
29
29
  */
30
30
  import { themeNotice, themeOptions } from "./appearance.js";
31
31
  import { qrSvg } from "./qr.js";
32
+ import { renderPersonasSettings } from "./persona-ui.js";
32
33
 
33
34
  export const DEN_SETTING_CATEGORIES = [
34
35
  {
@@ -628,6 +629,33 @@ export const DEN_SETTINGS = [
628
629
  read: (ctx) => effective(ctx.data.editors, ctx.data.editor),
629
630
  write: (v, ctx) => ctx.post({ editor: v || null }),
630
631
  },
632
+ {
633
+ id: "editor.open",
634
+ category: "editor",
635
+ label: "Open file links in",
636
+ description:
637
+ "⌘-click (Ctrl-click on Windows and Linux) on a file path in an agent's pane opens it here, at the line. None copies the path instead.",
638
+ keywords: [
639
+ "editor",
640
+ "link",
641
+ "path",
642
+ "file",
643
+ "agents",
644
+ "vscode",
645
+ "cursor",
646
+ "zed",
647
+ ],
648
+ scope: "user",
649
+ control: seg([
650
+ { value: "vscode", label: "VS Code" },
651
+ { value: "cursor", label: "Cursor" },
652
+ { value: "zed", label: "Zed" },
653
+ { value: "none", label: "None (copy)" },
654
+ ]),
655
+ keys: ["editor_open"],
656
+ read: (ctx) => ctx.data.editor_open ?? "none",
657
+ write: (v, ctx) => ctx.post({ editor_open: v }),
658
+ },
631
659
  {
632
660
  id: "editor_overrides",
633
661
  category: "editor",
@@ -813,6 +841,23 @@ export const DEN_SETTINGS = [
813
841
  },
814
842
 
815
843
  // ── Agents ─────────────────────────────────────────────────────────────
844
+ {
845
+ id: "agents.layout",
846
+ category: "agents",
847
+ label: "Agents tab layout",
848
+ description:
849
+ "Focus shows one agent, Tabs shows one with a tab for each, Tile shows up to four in a grid. The Agents tab's switch, ⌃⇧1, ⌃⇧2, ⌃⇧3 and ⌘K set it too.",
850
+ keywords: ["agents", "layout", "focus", "tabs", "tile", "grid", "panes"],
851
+ scope: "user",
852
+ control: seg([
853
+ { value: "focus", label: "Focus" },
854
+ { value: "tabs", label: "Tabs" },
855
+ { value: "tile", label: "Tile" },
856
+ ]),
857
+ keys: ["agents_layout"],
858
+ read: (ctx) => ctx.data.agents_layout ?? "focus",
859
+ write: (v, ctx) => ctx.post({ agents_layout: v }),
860
+ },
816
861
  {
817
862
  id: "agents.runtimes",
818
863
  category: "agents",
@@ -835,6 +880,27 @@ export const DEN_SETTINGS = [
835
880
  read: () => null,
836
881
  },
837
882
 
883
+ {
884
+ id: "agents.personas",
885
+ category: "agents",
886
+ label: "Personas",
887
+ description:
888
+ "Agent setups you can start from. Built-in ones ship with Den, distribution ones with your distribution, and yours live on this machine. View or Edit, Duplicate, + New persona; only your own can be edited or deleted.",
889
+ keywords: [
890
+ "persona",
891
+ "role",
892
+ "agent",
893
+ "model",
894
+ "version",
895
+ "permission",
896
+ "allowed commands",
897
+ "instructions",
898
+ ],
899
+ scope: "user",
900
+ control: { type: "custom", render: renderPersonasSettings },
901
+ read: () => null,
902
+ },
903
+
838
904
  // ── Command line ───────────────────────────────────────────────────────
839
905
  {
840
906
  id: "cli.local_dev",
@@ -26,6 +26,7 @@
26
26
  })();
27
27
  </script>
28
28
  <link rel="stylesheet" href="/theme.css" id="theme-css" />
29
+ <link rel="stylesheet" href="/persona-ui.css" />
29
30
  <title>__DEN_TITLE__ · Settings</title>
30
31
  <style>
31
32
  /* The Settings page (R21): the prototype's (den-shell.html) page
@@ -438,6 +438,7 @@
438
438
  "set.language": "Language",
439
439
  "set.appearance.org-theme": "Org default theme",
440
440
  "set.editor": "Default IDE",
441
+ "set.editor.open": "Open file links in",
441
442
  "set.editor_overrides": "Per-language IDE",
442
443
  "set.terminal": "Default terminal",
443
444
  "set.shell.pages": "Pages",
@@ -452,6 +453,7 @@
452
453
  "set.graph.panelWidth": "Details panel",
453
454
  "set.workspace.editor": "IDE for this workspace",
454
455
  "set.agents.runtimes": "Runtimes",
456
+ "set.agents.layout": "Agents tab layout",
455
457
  "set.cli.local_dev": "Local environment",
456
458
  "set.cli.cli_tabs": "Terminal tabs",
457
459
  "set.cli.log_window_lines": "Log pane lines",
@@ -552,6 +554,123 @@
552
554
  "ag.review": "Review",
553
555
  "ag.resumedAll": "Resumed {n} agents",
554
556
  "ag.resumedAll.one": "Resumed {n} agent",
557
+ "pe.configFirst": "Fix the config first: the personas are read from it.",
558
+ "ag.new.title": "Start an agent",
559
+ "rt.missingLine": "Not found on this machine:",
560
+ "rt.install": "Install…",
561
+ // The Start dialog and personas (AG-10 to AG-13).
562
+ "ag.new.what": "What's a persona?",
563
+ "ag.new.details": "Details: {n}",
564
+ "ag.new.hide": "Hide details",
565
+ "ag.new.newP": "+ New persona",
566
+ "ag.new.plain": "Plain session",
567
+ "ag.new.plainP": "A terminal in the workspace folder. No agent.",
568
+ "ag.new.task": "First instruction (optional)",
569
+ "ag.new.taskPh":
570
+ "e.g. Take the empty-state story. Match the design and add a test.",
571
+ "ag.new.adv": "Advanced: model and permissions",
572
+ "ag.new.advNote":
573
+ "Defaults come from the persona. Changes here apply to this session only, and the session is marked in the list.",
574
+ "ag.new.model": "Model",
575
+ "ag.new.modelDef": "Persona default ({m})",
576
+ "ag.new.ver": "Version",
577
+ "ag.new.verDef": "Persona default ({v})",
578
+ "ag.new.note":
579
+ "Starts in the workspace folder. It keeps running if you close the pane.",
580
+ "ag.new.started": "Started {p} in {w}",
581
+ "pm.ask": "Ask for everything",
582
+ "pm.edits": "Auto-approve edits in the worktree",
583
+ "pm.all": "Auto-approve all",
584
+ "pm.tag.ask": "asks for all",
585
+ "pm.tag.edits": "auto-approve edits",
586
+ "pm.tag.all": "auto-approve all",
587
+ "ov.runtime": "runtime: {r}",
588
+ "ov.model": "model: {m}",
589
+ "ov.latest": "latest",
590
+ "ov.tools": "custom tools",
591
+ "pe.what":
592
+ "A persona is a saved agent setup: a role, its instructions, a default runtime and model, and what it may do without asking. You can have several with the same role, each with its own name.",
593
+ "pe.preset": "Role preset",
594
+ "pe.extra": "Extra allowed commands",
595
+ "pe.extraS": "extra",
596
+ "pe.extraP":
597
+ "Plain command prefixes, one per line. No patterns or regex. Den translates them for each runtime.",
598
+ "pe.allows": "What this allows on {r}",
599
+ "pe.ns": "not supported on {r}",
600
+ "pe.nsMode": "{r} takes no permission mode from Den yet",
601
+ "pe.nsModel": "{r} takes no model from Den",
602
+ "pe.nsPat": "use a plain prefix, not a pattern",
603
+ "pe.nsBad": "not a plain prefix",
604
+ "pe.badCmd":
605
+ '"{c}" isn\'t a plain command prefix: use letters, digits, single spaces and . _ / : = @ % + , -',
606
+ "pe.raw": "Edit raw runtime config…",
607
+ "pe.rawLater": "Opening a runtime's own config file is coming later.",
608
+ "pe.vendor": "vendor-specific",
609
+ "pe.instr": "Instructions",
610
+ "pe.runtime": "Default runtime",
611
+ "pe.model": "Model",
612
+ "pe.ver": "Version",
613
+ "pe.rtDefault": "the runtime's default",
614
+ "pe.perms": "Permission mode",
615
+ "pe.noneCmd": "none",
616
+ "pe.loading": "Loading…",
617
+ "pe.loadFail": "The personas did not load.",
618
+ "pe.src.builtin": "built-in",
619
+ "pe.src.distribution": "distribution",
620
+ "pe.src.local": "mine",
621
+ "pe.src.hub": "hub",
622
+ "pe.src.builtin.p":
623
+ "Ships with Den. Read-only here; duplicate it to customize.",
624
+ "pe.src.distribution.p":
625
+ "Ships with your distribution. Read-only here; duplicate it to customize.",
626
+ "pe.src.local.p": "Yours, on this machine.",
627
+ "pe.src.hub.p": "Shared through Den Hub. Read-only here.",
628
+ "pe.new": "New persona",
629
+ "pe.dupT": "Duplicate persona",
630
+ "pe.edit": "Edit persona",
631
+ "pe.dup": "Duplicate",
632
+ "pe.editB": "Edit",
633
+ "pe.view": "View",
634
+ "pe.save": "Save persona",
635
+ "pe.del": "Delete",
636
+ "pe.deleted": "Deleted {n}",
637
+ "pe.name": "Name",
638
+ "pe.nameP": "e.g. Rita",
639
+ "pe.role": "Role",
640
+ "pe.roleHelp":
641
+ "A role sets this persona's starting template and lets Den hand work to it. It adds nothing hidden to the instructions.",
642
+ "pe.desc": "One-line description",
643
+ "pe.cat": "Category",
644
+ "pe.copy": "{n} (copy)",
645
+ "pe.saved": "Saved {n}",
646
+ "pe.ro": "Read-only here. Duplicate it to make a copy of your own.",
647
+ "pf.search": "Search personas by name, role or description",
648
+ "pf.cat": "Category",
649
+ "pf.src": "Source",
650
+ "pf.all": "All",
651
+ "pf.none": "No persona matches. Clear the filters or create one.",
652
+ "pf.clear": "Clear filters",
653
+ "cat.engineering": "Engineering",
654
+ "cat.design": "Design",
655
+ "cat.product": "Product",
656
+ "cat.marketing": "Marketing",
657
+ "cat.operations": "Operations",
658
+ "rt.installed": "Installed",
659
+ "rt.missing": "Not installed",
660
+ "rt.notInst": "{r} not installed",
661
+ "rt.later": "later",
662
+ "rt.fallback":
663
+ "{p}'s default runtime, {r}, isn't installed here, so this starts on {f}.",
664
+ "ver.latest": "latest",
665
+ "ver.latestP": "Latest (tracks the newest release)",
666
+ "ver.newer": "newer available",
667
+ "ver.newerP":
668
+ "A newer version of this model is out. This one stays pinned until you change it.",
669
+ "ver.newerOf": "Newer: {v}",
670
+ "ver.use": "Use latest",
671
+ "ver.pinned": "pinned {v}",
672
+ "ver.usedLatest": "{n} now tracks the latest version",
673
+ "set.agents.personas": "Personas",
555
674
  // The Agents tab (AG-4a-term, agents-page.js).
556
675
  "n.agents": "Agents",
557
676
  "kind.agent": "Agent",
@@ -585,6 +704,24 @@
585
704
  "ag.pal": "Agents › {ws} / {name}",
586
705
  "ag.palStart": "Start agent…",
587
706
  "ag.cap": "Up to 4 panes: {id}'s closed to make room (it keeps running)",
707
+ // AG-4b: the Agents tab's layouts and named tabs.
708
+ "ag.layout": "Layout",
709
+ "ag.l.focus": "Focus",
710
+ "ag.l.tabs": "Tabs",
711
+ "ag.l.tile": "Tile",
712
+ "ag.l.tip": "{name} ({key})",
713
+ "ag.hint.keys":
714
+ "⌃⇧1 / ⌃⇧2 / ⌃⇧3 switch layout. ⌘-click (Ctrl-click) opens a link or path.",
715
+ "ag.pal.layout": "Layout: {name}",
716
+ "ag.copied": "Copied",
717
+ "ag.tabs": "Open sessions",
718
+ "ag.hint.focus":
719
+ "Click shows one session. Shift- or ⌘-click adds it alongside.",
720
+ "ag.hint.tabs":
721
+ "Click opens a session as a tab. Double-click a tab to rename it.",
722
+ "ag.hint.tile": "Click adds a session as a tile, up to 4.",
723
+ "ag.renameTip": "Double-click to rename",
724
+ "ag.renameName": "Rename {id}",
588
725
  "ag.rt.ready": "Starts {name} {version} from",
589
726
  "ag.rt.readyNoVer": "Starts {name} from",
590
727
  "ag.rt.notFound": "{name} not found.",
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The Agents tab's terminal input and links (AG-4b), pure helpers for
3
+ * agents-page.js (and its tests):
4
+ *
5
+ * isWebUrl(uri) only http: and https: open (an OSC 8 link's
6
+ * javascript:, data:, file: and the rest never)
7
+ * linkModifier(e, mac) the click that opens a link: ⌘ on a Mac, Ctrl
8
+ * elsewhere; a plain click still selects
9
+ * findPaths(line) file paths in a line of output: `src/foo.ts:42`,
10
+ * `./a/b.js:3:7`, `/abs/path`; never a URL
11
+ * editorUrl(ed, abs, l) an editor's URL for a path and line (vscode,
12
+ * cursor, zed); none: null (the path is copied)
13
+ * softNewlineKey(e) Shift+Enter, and no other modifier
14
+ * layoutKeyIndex(e) ⌃⇧1 / ⌃⇧2 / ⌃⇧3 (by the key's code: Shift+1 is
15
+ * "!" on a US keyboard): 0-2, else -1. ⌘1-9 and
16
+ * Ctrl+1-9 are the browser's (its tabs), never ours
17
+ */
18
+
19
+ /** The editors a path link opens in, by URL scheme (Settings › editor.open). */
20
+ export const EDITORS = ["vscode", "cursor", "zed", "none"];
21
+
22
+ /** Whether `uri` is an http or https URL: the only links that open. */
23
+ export function isWebUrl(uri) {
24
+ if (typeof uri !== "string" || !uri) return false;
25
+ let u;
26
+ try {
27
+ u = new URL(uri);
28
+ } catch {
29
+ return false;
30
+ }
31
+ return (u.protocol === "http:" || u.protocol === "https:") && !!u.host;
32
+ }
33
+
34
+ /** This browser is on a Mac (⌘ opens links there, Ctrl elsewhere). */
35
+ export function isMac(nav = globalThis.navigator) {
36
+ const p = nav?.userAgentData?.platform || nav?.platform || "";
37
+ return /mac|iphone|ipad/i.test(p);
38
+ }
39
+
40
+ /** The click opens a link: ⌘-click on a Mac, Ctrl-click elsewhere. */
41
+ export function linkModifier(e, mac = isMac()) {
42
+ if (!e) return false;
43
+ return mac ? !!e.metaKey : !!e.ctrlKey;
44
+ }
45
+
46
+ /**
47
+ * A path, then an optional :line and :column. It never starts right after
48
+ * a word character, a dot, a slash or a colon, so no part of a URL
49
+ * (https://host/a/b) and no part of a longer word is one.
50
+ */
51
+ const PATH_RE =
52
+ /(?<![\w./~:@%+-])((?:\.{1,2}\/|\/)?[\w@+-][\w.@+-]*(?:\/[\w.@+-]+)*)(?::(\d+)(?::(\d+))?)?/g;
53
+ const EXT_RE = /[^/.]\.[A-Za-z0-9]{1,12}$/;
54
+
55
+ /**
56
+ * The file paths in `line`: { text, start, end, path, line?, col? }, start
57
+ * and end its columns in the line (end exclusive). A path has a "/" and
58
+ * either starts with /, ./ or ../, has an extension or a line; or it is a
59
+ * file name with an extension and a line (`foo.ts:12`). The server says
60
+ * which of them exist in the agent's workspace (only those are links).
61
+ */
62
+ export function findPaths(line) {
63
+ const out = [];
64
+ if (typeof line !== "string" || !line) return out;
65
+ PATH_RE.lastIndex = 0;
66
+ for (let m; (m = PATH_RE.exec(line)); ) {
67
+ let p = m[1];
68
+ const ln = m[2] === undefined ? undefined : Number(m[2]);
69
+ const col = m[3] === undefined ? undefined : Number(m[3]);
70
+ let text = m[0];
71
+ if (ln === undefined) {
72
+ // A sentence's full stop is not the path's.
73
+ const trimmed = p.replace(/\.+$/, "");
74
+ text = trimmed;
75
+ p = trimmed;
76
+ }
77
+ if (!p) continue;
78
+ const slash = p.includes("/");
79
+ const rooted = /^(?:\/|\.{1,2}\/)/.test(p);
80
+ const ext = EXT_RE.test(p);
81
+ const ok = slash
82
+ ? rooted || ext || ln !== undefined
83
+ ? !/^\d+\/\d+$/.test(p)
84
+ : false
85
+ : ext && ln !== undefined;
86
+ if (!ok) continue;
87
+ out.push({
88
+ text,
89
+ start: m.index,
90
+ end: m.index + text.length,
91
+ path: p,
92
+ ...(ln !== undefined ? { line: ln } : {}),
93
+ ...(col !== undefined ? { col } : {}),
94
+ });
95
+ }
96
+ return out;
97
+ }
98
+
99
+ /**
100
+ * The URL that opens `abs` at `line` (and `col`) in `editor`, or null:
101
+ * none, an editor Den doesn't know, or a path that isn't absolute.
102
+ */
103
+ export function editorUrl(editor, abs, line, col) {
104
+ if (!["vscode", "cursor", "zed"].includes(editor)) return null;
105
+ if (typeof abs !== "string" || !abs.startsWith("/")) return null;
106
+ const p = abs.split("/").map(encodeURIComponent).join("/");
107
+ const at = line ? `:${line}${col ? `:${col}` : ""}` : "";
108
+ return `${editor}://file${p}${at}`;
109
+ }
110
+
111
+ /** Shift+Enter, no other modifier: the soft newline. */
112
+ export function softNewlineKey(e) {
113
+ return (
114
+ e?.type === "keydown" &&
115
+ e.key === "Enter" &&
116
+ e.shiftKey &&
117
+ !e.ctrlKey &&
118
+ !e.altKey &&
119
+ !e.metaKey &&
120
+ !e.isComposing
121
+ );
122
+ }
123
+
124
+ /** ⌃⇧1, ⌃⇧2, ⌃⇧3: the layouts' index; else -1. */
125
+ export function layoutKeyIndex(e) {
126
+ if (e?.type !== "keydown") return -1;
127
+ if (!e.ctrlKey || !e.shiftKey || e.metaKey || e.altKey) return -1;
128
+ return ["Digit1", "Digit2", "Digit3"].indexOf(e.code);
129
+ }
@@ -877,14 +877,32 @@ function paletteItems() {
877
877
  });
878
878
  }
879
879
  for (const x of palSessions) {
880
- const name = x.kind === "agent" ? (x.persona ?? x.label) : x.label;
880
+ // AG-4b: the name a person gave the agent, else its persona.
881
+ const base =
882
+ x.kind === "agent" ? x.displayName || (x.persona ?? x.label) : x.label;
883
+ // AG-4b: the session's name (a /rename, or Den's), when it says more.
884
+ const sn = x.kind === "agent" ? (x.sessionTitle ?? "").trim() : "";
885
+ const extra =
886
+ sn && sn !== base && sn !== x.persona && sn !== x.label ? sn : "";
887
+ const name = extra ? `${base} · ${extra}` : base;
881
888
  items.push({
882
889
  key: `agent:${x.id}`,
883
890
  kind: "agent",
884
891
  label: t("ag.pal", { ws: x.workspace, name }),
885
- sub: x.kind === "agent" && x.persona !== x.label ? x.label : undefined,
886
- dot: x.state === "running" ? "" : "off",
887
- keywords: [x.workspace, x.label, x.persona ?? "", "agents", "terminal"],
892
+ sub:
893
+ x.kind === "agent" && (x.displayName || x.persona !== x.label)
894
+ ? x.label
895
+ : undefined,
896
+ dot: x.waiting ? "warn" : x.state === "running" ? "" : "off",
897
+ keywords: [
898
+ x.workspace,
899
+ x.label,
900
+ x.persona ?? "",
901
+ x.displayName ?? "",
902
+ x.sessionTitle ?? "",
903
+ "agents",
904
+ "terminal",
905
+ ],
888
906
  run: () => go(`/agents?open=${encodeURIComponent(x.id)}`),
889
907
  });
890
908
  }
@@ -148,6 +148,21 @@ export const ROUTES: readonly RouteEntry[] = [
148
148
  "/api/agents/runtimes",
149
149
  "/api/workspaces/:ws/agents",
150
150
  ].map((p) => R(p)),
151
+ R("/api/personas/:id", "one persona with its instructions (AG-10.6 Details)"),
152
+ M(
153
+ ["POST"],
154
+ "/api/personas",
155
+ "New persona: a person's own, in the state dir (AG-10.7); never an agent's token",
156
+ ),
157
+ M(
158
+ ["POST", "DELETE"],
159
+ "/api/personas/:id",
160
+ "edits or deletes a person's own persona (AG-10.7); never an agent's token",
161
+ ),
162
+ R(
163
+ "/api/workspaces/:ws/agents/:id/paths",
164
+ "which paths an agent printed exist in its workspace (stat only, AG-4b)",
165
+ ),
151
166
  R(
152
167
  "/api/sessions",
153
168
  "lists the sessions and agents (names and states, never their output)",
@@ -302,6 +317,11 @@ export const ROUTES: readonly RouteEntry[] = [
302
317
  M(["POST", "DELETE"], "/api/workspaces/services"),
303
318
  M(["POST"], "/api/workspaces/settings"),
304
319
  M(["POST"], "/api/workspaces/:ws/agents/:id/stop", "TERM to its session"),
320
+ M(
321
+ ["POST"],
322
+ "/api/workspaces/:ws/agents/:id/name",
323
+ "the name a person gave an agent (AG-4b); its id never changes",
324
+ ),
305
325
  M(
306
326
  ["POST"],
307
327
  "/api/workspaces/:ws/agents/:id/remove",
@@ -333,6 +353,11 @@ export const ROUTES: readonly RouteEntry[] = [
333
353
  "/api/workspaces/:ws/agents",
334
354
  "starts an agent: a runtime in a PTY",
335
355
  ),
356
+ S(
357
+ ["POST"],
358
+ "/api/workspaces/:ws/sessions",
359
+ "starts a plain session: the person's login shell in the workspace (AG-10.1)",
360
+ ),
336
361
  S(
337
362
  ["POST"],
338
363
  "/api/workspaces/:ws/agents/:id/resume",
@@ -407,8 +432,12 @@ export const REGEX_ROUTES: Readonly<Record<string, readonly string[]>> = {
407
432
  "/api/workspaces/:ws/agents/:id/fresh",
408
433
  "/api/workspaces/:ws/agents/:id/relaunch",
409
434
  "/api/workspaces/:ws/agents/:id/remove",
435
+ "/api/workspaces/:ws/agents/:id/name",
436
+ "/api/workspaces/:ws/agents/:id/paths",
410
437
  ],
411
438
  "agent-routes.ts:RUNTIME_PATH": ["/api/agents/runtimes/:id/path"],
439
+ "agent-routes.ts:PERSONA_PATH": ["/api/personas/:id"],
440
+ "agent-routes.ts:PLAIN_PATH": ["/api/workspaces/:ws/sessions"],
412
441
  "session-routes.ts:IO_PATH": ["/api/sessions/:id/io"],
413
442
  "github-routes.ts:SIGNIN_PATH": ["/api/github/signin/:id"],
414
443
  "github-routes.ts:LINK_PATH": ["/api/web/owner/link/:id"],
@@ -43,6 +43,8 @@ import { getStateDir } from "../../shared/paths.js";
43
43
  import { product } from "../../shared/product.js";
44
44
  import { revokedAgentIds } from "../../shared/actor-token.js";
45
45
  import { listRecords } from "../../shared/agents/records.js";
46
+ import { softNewlineSeq } from "../../shared/agents/catalog.js";
47
+ import { getRuntime } from "../../shared/agents/runtimes.js";
46
48
  import type { RotationInfo } from "../../shared/agents/rotation.js";
47
49
  import {
48
50
  connectHost,
@@ -53,7 +55,8 @@ import { parseSessionName } from "../../shared/sessions/names.js";
53
55
  import { sessionPaths } from "../../shared/sessions/paths.js";
54
56
  import { loadPty, type PtyLoad } from "../../shared/sessions/pty.js";
55
57
  import type { SessionInfo } from "../../shared/sessions/types.js";
56
- import { agentRows, noteEnded } from "../cli/agent-ops.js";
58
+ import { agentRows, newerOf, noteEnded } from "../cli/agent-ops.js";
59
+ import type { Override } from "../../shared/agents/records.js";
57
60
  import { state as sessionState } from "../cli/sessions-command.js";
58
61
  import { recordAction } from "./audit.js";
59
62
  import { sendJson } from "./http-helpers.js";
@@ -132,9 +135,31 @@ export interface ListedSession {
132
135
  /** The `<name>` part: an agent's id, a plain session's name. */
133
136
  label: string;
134
137
  agent?: string;
138
+ /** The name a person gave the agent (AG-4b), when one did. */
139
+ displayName?: string;
140
+ /**
141
+ * The name its session shows (AG-4b): the latest of Den's rename and the
142
+ * runtime's (Claude Code's /rename), when there is one. Its handle is
143
+ * `name`.
144
+ */
145
+ sessionTitle?: string;
146
+ /**
147
+ * What Shift+Enter sends to it (AG-4b, its catalog entry's softNewline):
148
+ * ESC CR unless the runtime says otherwise. A plain session: none (Enter).
149
+ */
150
+ softNewline?: string;
135
151
  persona?: string;
136
152
  runtime?: string;
137
153
  model?: string;
154
+ /** A pinned model version (AG-12), and the newer one when there is. */
155
+ version?: string;
156
+ newer?: string;
157
+ /** Its permission intent (AG-13): ask, edits, all. */
158
+ permission?: string;
159
+ /** Extra allowed commands it was started with (AG-13.3). */
160
+ allow?: string[];
161
+ /** What it overrides of its persona (AG-10.9): its row's tags. */
162
+ overrides?: Override[];
138
163
  cwd?: string;
139
164
  /** running, stopping, exited (<code>), stopped, interrupted (AG-14). */
140
165
  state: string;
@@ -193,9 +218,17 @@ export function listedSessions(
193
218
  kind: "agent",
194
219
  label: rec.id,
195
220
  agent: rec.id,
221
+ ...(rec.displayName ? { displayName: rec.displayName } : {}),
222
+ ...(r.sessionTitle ? { sessionTitle: r.sessionTitle } : {}),
223
+ softNewline: softNewlineSeq(getRuntime(rec.runtime)?.catalog),
196
224
  persona: rec.persona,
197
225
  runtime: rec.runtime,
198
226
  ...(rec.model ? { model: rec.model } : {}),
227
+ ...(rec.version ? { version: rec.version } : {}),
228
+ ...newerOf(rec),
229
+ ...(rec.permission ? { permission: rec.permission } : {}),
230
+ ...(rec.allow?.length ? { allow: [...rec.allow] } : {}),
231
+ ...(rec.overrides?.length ? { overrides: [...rec.overrides] } : {}),
199
232
  cwd: rec.cwd,
200
233
  state: r.state,
201
234
  exited: !s || s.exited === true,
@@ -36,6 +36,11 @@ export const UI_MODULES: readonly string[] = [
36
36
  "/agents-page.js",
37
37
  // The Start agent form's choices, shared by both (AG-10.0).
38
38
  "/agent-start.js",
39
+ // AG-10.4-10.7: the persona picker, editor and Settings › Personas.
40
+ "/persona-ui.js",
41
+ "/persona-ui.css",
42
+ // AG-4b: the panes' keys and links (Shift+Enter, ⌃⇧1-3, paths).
43
+ "/term-input.js",
39
44
  // SEC-2a: Pair a device's QR code (a tiny encoder of Den's own).
40
45
  "/qr.js",
41
46
  // The UI's catalogs other than English (R19; en is in shell.js).
@@ -51,6 +56,9 @@ export const VENDOR_ASSETS: Readonly<Record<string, string>> = {
51
56
  "/vendor/xterm.mjs": "@xterm/xterm/lib/xterm.mjs",
52
57
  "/vendor/xterm.css": "@xterm/xterm/css/xterm.css",
53
58
  "/vendor/addon-fit.mjs": "@xterm/addon-fit/lib/addon-fit.mjs",
59
+ // AG-4b: plain URLs in a pane are links (⌘- or Ctrl-click opens them).
60
+ "/vendor/addon-web-links.mjs":
61
+ "@xterm/addon-web-links/lib/addon-web-links.mjs",
54
62
  };
55
63
 
56
64
  /** The installed file for a vendored URL path, or null. */