@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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,175 @@ All notable changes to Den are documented here. Each release lists the
4
4
  config and manifest changes a distribution must make to move to it under
5
5
  **Migrations**.
6
6
 
7
+ ## [0.4.13] - 2026-11-07
8
+
9
+ ### Added
10
+
11
+ - **The Start dialog.** + Start agent opens the dialog from the Den Shell
12
+ design. Pick a persona from a list you can search, with category and
13
+ source chips (built-in, distribution, mine); with no filter it is grouped
14
+ by category, and Plain session (your shell in the workspace, no agent) is
15
+ pinned last. Each persona shows its one-line description. "What's a
16
+ persona?" explains the idea, and Details shows the instructions, runtime,
17
+ model and version, permission mode and allowed commands. Then Runtime,
18
+ Workspace, an optional first instruction, and a collapsed Advanced
19
+ section: Model and Version, Permission mode, and Extra allowed commands,
20
+ with a preview of exactly what the runtime will be given.
21
+ - **First instruction.** Den types it into the agent's session once the
22
+ session is ready, as a bracketed paste when the runtime asks for one,
23
+ then Enter. It never goes on the command line and is never stored; the
24
+ audit log records its length only.
25
+ - **Your own personas.** + New persona, Duplicate and Edit write personas
26
+ of your own to `<state_dir>/personas/<id>.md` (0600). Settings ›
27
+ Personas lists every persona with the same search and chips: View or
28
+ Edit, Duplicate, Delete (your own only), + New persona. Built-in and
29
+ distribution personas are read-only; Duplicate makes your own copy.
30
+ Routes: `GET /api/personas/<id>`, `POST /api/personas`, `POST` and
31
+ `DELETE /api/personas/<id>`.
32
+ - **Models and versions.** A runtime lists its models (den-claude: opus,
33
+ sonnet and haiku, each "latest" or a pinned version, from a catalog
34
+ shipped with Den). A pinned version runs as `--model <full id>`. A
35
+ persona or session pinned to an older version says "newer available";
36
+ your own persona has Use latest.
37
+ - **Permissions as intent.** Ask for everything, Auto-approve edits in the
38
+ worktree, or Auto-approve all, plus extra allowed commands as plain
39
+ prefixes. den-claude turns them into `--permission-mode` and
40
+ `Bash(<prefix>)` / `Bash(<prefix> *)` allow rules beside Den's deny
41
+ rules. Other runtimes take none from Den yet, and the dialog says "not
42
+ supported on <runtime>" rather than dropping one.
43
+ - **Override tags.** A session started with choices that differ from its
44
+ persona is tagged in the list: "model: haiku", "pinned <version>",
45
+ "auto-approve edits", "custom tools", the runtime. "auto-approve all" is
46
+ always tagged. Start fresh and Relaunch keep the choices.
47
+ - **Roles and categories.** Den's roles are planner, orchestrator, sde,
48
+ reviewer, tester and designer. Each is a template (instructions,
49
+ permission mode, a read-only command preset) that fills in New persona
50
+ without overwriting what you typed, and it adds nothing hidden to a
51
+ prompt. A distribution adds roles and categories (`agents.roles`,
52
+ `agents.categories`) and may extend a Den role's preset, never redefine
53
+ it.
54
+ - **Plain session from the page.** `POST /api/workspaces/<ws>/sessions`
55
+ starts your login shell in the workspace's folder (capped by
56
+ `sessions.max_concurrent`).
57
+ - **A tester persona** in Den's default pack.
58
+
59
+ ### Changed
60
+
61
+ - `POST /api/workspaces/<ws>/agents` takes `version`, `permission`,
62
+ `allow` and `instruction`, and refuses an unknown field. Each is checked
63
+ against the runtime's catalog: a model or version it doesn't list, a
64
+ mode other than `ask`, `edits` or `all`, or a command that isn't a plain
65
+ prefix (at most 32, 120 characters each) is a 400. An agent's token gets
66
+ 403 on every persona route and on a plain session.
67
+ - The agent record keeps `version`, `permission`, `allow` and `overrides`.
68
+ - A persona's `role` is a role key; a sentence there still reads as its
69
+ description. Personas gain `display_name`, `category`, `version` and
70
+ `allowed_commands`. `permission_mode` takes `ask`, `edits` or `all`
71
+ (Claude Code's own modes still read). `agents personas` shows SOURCE and
72
+ ROLE columns.
73
+ - fe-dev, be-dev and devops are role `sde`; ux-designer is role
74
+ `designer` in category Design.
75
+ - A persona whose runtime isn't installed starts on the first one that is,
76
+ and the Start dialog says so.
77
+ - **`sessionTitle` is the name a session shows.** `agents list --json`,
78
+ `GET /api/sessions` and the start and resume answers (MCP
79
+ `agent_resume`) now carry the resolved name (Den's rename or the
80
+ runtime's, the later) as `sessionTitle`. 0.4.12 called it `sessionName`,
81
+ which is the agent record's session handle (`<dist>-<ws>:<id>`) and has
82
+ been since 0.4.0. Records are unchanged.
83
+
84
+ ### Fixed
85
+
86
+ - **Settings shows a saved choice at once.** Agents tab layout and Open
87
+ file links in saved, but the pressed button stayed on the old value
88
+ until a reload. The page now takes every setting the save returns.
89
+ - **Claude Code's `/rename "x"` shows as x.** Its title keeps the quotes;
90
+ Den drops one pair around the whole name, for display only.
91
+ - **The terminal fit check is exact again.** The browser test compares
92
+ what is left below and beside a terminal with the cell size xterm drew
93
+ with (now on each pane as `data-cell-w` and `data-cell-h`), less than
94
+ one cell, so a missed row fails on any font.
95
+
96
+ ### Migrations
97
+
98
+ - None required. A persona whose `role:` is a sentence keeps working (it
99
+ becomes the description); set `role:` to a role key to give it one.
100
+ - A persona pack may now set `display_name`, `category`, `version` and
101
+ `allowed_commands`. Use `permission_mode: ask | edits | all` for a mode
102
+ every runtime can translate.
103
+ - `agents.roles` and `agents.categories` are new keys of `agents:` in the
104
+ distribution's config (see docs/distributions.md, Personas).
105
+ - Read `sessionTitle`, not `sessionName`, for the name a session shows in
106
+ `agents list --json`, `GET /api/sessions` and the start and resume
107
+ answers. No record needs changing.
108
+
109
+ ## [0.4.12] - 2026-11-07
110
+
111
+ ### Added
112
+
113
+ - **Agents tab layouts.** A switch in the page header picks Focus, Tabs or
114
+ Tile, and so do ⌃⇧1, ⌃⇧2 and ⌃⇧3 (Ctrl+Shift+digit on every OS) and ⌘K
115
+ "Layout: Focus/Tabs/Tile". ⌘1-9 and Ctrl+1-9 stay the browser's, for its
116
+ tabs; inside a terminal the layout keys never reach the agent. Focus, the default, shows one pane: a
117
+ click replaces it, and Shift-click or ⌘-click adds one alongside. Tabs
118
+ shows one pane under a strip of tabs. Tile is a grid: one pane fills
119
+ it, two sit side by side, three or four make a 2×2. At phone width,
120
+ Focus and Tile show as Tabs. Your choice is saved for you
121
+ (`agents.layout` in `config/local/user.yml`, `agents_layout` in
122
+ `/api/settings`, and Settings › Agents), and the URL says it
123
+ (`&layout=`).
124
+ - **Named tabs.** Each tab shows the agent's name, then its persona and
125
+ workspace, and a dot when it is waiting. Double-click a tab to rename
126
+ the agent. The name is stored in its record (`displayName`) and shows in
127
+ the list, its pane, ⌘K, the Workspaces page and `agents list --json`.
128
+ Start fresh and Relaunch keep it. The id never changes. The route is
129
+ `POST /api/workspaces/<ws>/agents/<id>/name`.
130
+ - **Shift+Enter is a soft newline in agent terminals.** It sends the
131
+ runtime's `softNewline` from the runtime catalog, not Enter: ESC CR for
132
+ Claude Code and Gemini CLI (and by default), Ctrl+J for Codex CLI. A
133
+ plain session's Shift+Enter is still Enter. Catalog entries may set
134
+ `softNewline: esc-cr | lf | none`.
135
+ - **The session's name in the pane header.** Claude Code's `/rename` shows
136
+ after the runtime (` · api spike`), and in the tab, the list and ⌘K. Den
137
+ reads it from the session's transcript, following the file from where it
138
+ last stopped (the catalog marks this source `unofficial`; a name it
139
+ can't read is none). The most recent rename wins, Den's or the
140
+ runtime's. `GET /api/sessions`, `agents list --json` and the start and
141
+ resume answers carry it as `sessionName`. Catalog entries may set
142
+ `titleFromOsc` (no runtime does yet, and the session host doesn't read
143
+ titles yet).
144
+ - **Links and paths in agent panes are clickable.** ⌘-click (Ctrl-click on
145
+ Windows and Linux) opens a URL or an OSC 8 link in a new tab
146
+ (`noopener`), http and https only; a plain click still selects. A file
147
+ path inside the agent's workspace (`src/foo.ts:42`, absolute paths) is
148
+ a link: ⌘-click opens it in the editor set in Settings › Editor &
149
+ terminal › "Open file links in" (`editor_open`: vscode, cursor, zed, or
150
+ none to copy the path, the default). The server checks a path exists
151
+ inside the workspace with `GET /api/workspaces/<ws>/agents/<id>/paths`
152
+ (stat only). Den now ships `@xterm/addon-web-links` 0.12.0, served from
153
+ `/vendor/`.
154
+
155
+ ### Changed
156
+
157
+ - **The Agents tab fills the window.** The list and the panes reach down to
158
+ 14px above the runs dock at any window height, with the dock open or
159
+ shut. They used to stop at a fixed guess of the window less 300px.
160
+ - **Re-picking the workspace in view on a page with `?ws=`** (Workspaces'
161
+ detail pane) now updates `?ws=` before the reload, so the page shows the
162
+ pick and not the old workspace.
163
+ - **Every terminal fits its pane.** A terminal used to be sized for its
164
+ pane's padding as well, so its last row could be cut off. It now fits
165
+ the space inside the padding, in every layout, and refits when the
166
+ window or the dock changes size.
167
+
168
+ ### Migrations
169
+
170
+ None. The record's new `displayName` and `displayNameAt` are optional:
171
+ records without them read as before. The layout pref is optional and
172
+ defaults to Focus; `editor_open` is optional and defaults to none. The
173
+ catalog's new `softNewline`, `sessionName` and `titleFromOsc` are
174
+ optional.
175
+
7
176
  ## [0.4.11] - 2026-11-07
8
177
 
9
178
  ### Changed
package/README.md CHANGED
@@ -10,8 +10,8 @@
10
10
  </p>
11
11
 
12
12
  <picture>
13
- <source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.11/hero-dark.png">
14
- <img src="docs/images/0.4.11/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
13
+ <source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.13/hero-dark.png">
14
+ <img src="docs/images/0.4.13/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
15
15
  </picture>
16
16
 
17
17
  ## Set up by your agent
@@ -38,7 +38,7 @@ It doesn't replace your tools. Your repos, Docker, your test runners, your secre
38
38
  yours. Den connects them and turns on good practice from the start.
39
39
 
40
40
  <p align="center">
41
- <img src="docs/images/0.4.11/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
41
+ <img src="docs/images/0.4.13/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
42
42
  </p>
43
43
 
44
44
  ## How it helps
@@ -68,10 +68,10 @@ yours. Den connects them and turns on good practice from the start.
68
68
  lives in your own repo.
69
69
 
70
70
  <p align="center">
71
- <img src="docs/images/0.4.11/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
72
- <img src="docs/images/0.4.11/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
73
- <img src="docs/images/0.4.11/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
74
- <img src="docs/images/0.4.11/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
71
+ <img src="docs/images/0.4.13/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
72
+ <img src="docs/images/0.4.13/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
73
+ <img src="docs/images/0.4.13/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
74
+ <img src="docs/images/0.4.13/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
75
75
  </p>
76
76
 
77
77
  ## Get started
@@ -2054,10 +2054,25 @@ server scoped to its workspace.
2054
2054
  unique in the distribution, so a second workspace's planner gets
2055
2055
  `planner-<ws>` unless you give `--id`.
2056
2056
  - The **agent record**, `<stateDir>/agents/<id>.json` (0600), holds its id,
2057
- persona, runtime, model, workspace, cwd, session and times. It never holds
2058
- the environment or the command line. `list` reads it for persona, runtime
2057
+ persona, runtime, model, workspace, cwd, session and times, and what it was
2058
+ started with from the Start dialog: `version` (the model's, "latest" or
2059
+ pinned), `permission` (`ask`, `edits` or `all`), `allow` (extra allowed
2060
+ commands) and `overrides` (which of these differ from its persona). Start
2061
+ fresh and Relaunch keep them. It never holds the environment, the command
2062
+ line or a first instruction. `list` reads it for persona, runtime
2059
2063
  and model; a record whose session has gone is listed as `stopped` (after
2060
2064
  `stop`) or `ended`.
2065
+ - **Two name fields.** In the record, `sessionName` is the session's
2066
+ handle, `<dist>-<ws>:<id>` (what `sessions attach` takes); it has meant
2067
+ that since 0.4.0, and `displayName` is the name a person gave the agent.
2068
+ In `agents list --json`, `GET /api/sessions` and the start and resume
2069
+ answers (MCP `agent_resume`), `sessionTitle` is the name its session
2070
+ shows: the latest of Den's rename and the runtime's own (Claude Code's
2071
+ `/rename`, one pair of surrounding quotes dropped), absent when there is
2072
+ none. The handle there is `name` (`/api/sessions`) or `session` (the
2073
+ start and resume answers). 0.4.12 sent the name shown as `sessionName`;
2074
+ read `sessionTitle` instead. Records are unchanged, so none written
2075
+ before needs migrating.
2061
2076
  - `list` shows agents only: sessions with an agent record. A session from
2062
2077
  `sessions start` is not an agent; `sessions list` shows it.
2063
2078
  - `stop` sends its session TERM and marks it stopped. `resume` works only
@@ -2104,13 +2119,78 @@ started. The workspace header counts them ("2 agents").
2104
2119
  workspace, and **Open** on a row with a session opens its terminal there.
2105
2120
 
2106
2121
  The routes: `GET /api/workspaces/<ws>/agents`, `POST
2107
- /api/workspaces/<ws>/agents` (`{ persona, runtime?, model?, id? }`), `POST
2108
- /api/workspaces/<ws>/agents/<id>/stop` and `…/resume`, and `GET
2109
- /api/personas` (personas and runtimes). Errors are the usual
2122
+ /api/workspaces/<ws>/agents` (`{ persona, runtime?, model?, version?,
2123
+ permission?, allow?, instruction?, id? }`, see
2124
+ [The Start dialog](#the-start-dialog)), `POST
2125
+ /api/workspaces/<ws>/agents/<id>/stop` and `…/resume`, `POST
2126
+ /api/workspaces/<ws>/sessions` (a plain session), `GET /api/personas`
2127
+ (personas, runtimes with their models and permission translation, roles
2128
+ and categories), and the persona routes under
2129
+ [Personas](#personas). Errors are the usual
2110
2130
  `{ ok: false, error, code, requestId }`: `cap` (429),
2111
2131
  `conflict` (409, e.g. already running, or no resume), `not_found` (404),
2112
2132
  `exited_at_once` (502, with the session's last lines).
2113
2133
 
2134
+ ### The Start dialog
2135
+
2136
+ **+ Start agent** on the Agents tab (or `/agents?start=1`) opens the Start
2137
+ dialog:
2138
+
2139
+ - **Persona**: a list you can search (name, role, description), with
2140
+ category chips (only the categories that have personas) and source chips
2141
+ (built-in, distribution, mine). With no filter set it is grouped by
2142
+ category. Each row is the persona's name ("Rita (reviewer)", else its
2143
+ id), its source badge and its one-line description, and "not installed"
2144
+ when its runtime isn't. **Plain session** is pinned last: your login
2145
+ shell in the workspace's folder, no agent. **What's a persona?** explains
2146
+ the idea; **Details** shows the selected persona's instructions, runtime,
2147
+ model and version, permission mode, role preset and extra commands.
2148
+ **+ New persona**, **Duplicate** and (for your own) **Edit** open the
2149
+ persona editor, and the dialog picks what you saved.
2150
+ - **Runtime** and **Workspace**. Installed runtimes come first; the rest
2151
+ are listed, disabled, and named under the field ("Not found on this
2152
+ machine: …"). A persona whose runtime isn't installed starts on the first
2153
+ one that is, and the dialog says so. The line under the runtime says what
2154
+ will run (see below).
2155
+ - **First instruction** (optional, up to 8000 characters): the agent's
2156
+ first message. Den types it into the agent's session once the session is
2157
+ ready (it has printed and gone quiet, or after 30 s), through the session
2158
+ host, as a bracketed paste when the runtime asked for one, then Enter. It
2159
+ is never a command-line argument and never stored; the audit log records
2160
+ its length only.
2161
+ - **Advanced** (collapsed): **Model** and **Version** (the runtime's own
2162
+ list; "Persona default" first), the **Permission mode** (Ask for
2163
+ everything, Auto-approve edits in the worktree, Auto-approve all), the
2164
+ role's command preset as chips, **Extra allowed commands** (plain command
2165
+ prefixes, one per line, no patterns), and a preview of exactly what the
2166
+ runtime will be given. A command a runtime can't take says "not supported
2167
+ on <runtime>", and a pattern says "use a plain prefix"; neither is ever
2168
+ dropped silently. "Edit raw runtime config…" is not there yet.
2169
+
2170
+ What you change in Advanced applies to that launch only, and the session's
2171
+ row is tagged: "model: haiku", "pinned claude-opus-4-8", "auto-approve
2172
+ edits", "custom tools", the runtime, and always "auto-approve all" for an
2173
+ agent started so. An agent pinned to an older version says "newer
2174
+ available".
2175
+
2176
+ Every field is checked on the server, not only in the page. The model and
2177
+ version must be ones the runtime lists, the permission one of `ask`,
2178
+ `edits` or `all`, and `allow` a list of at most 32 plain prefixes (letters,
2179
+ digits, single spaces and `. _ / : = @ % + , -`, at most 120 characters
2180
+ each). An unknown field is refused. A person's start only: an agent's
2181
+ token gets 403.
2182
+
2183
+ **How each runtime takes the choices.** den-claude (Claude Code): a model is
2184
+ `--model <alias>` (`opus`, `sonnet`, `haiku`), a pinned version is `--model
2185
+ <full id>`. The model list is a catalog shipped with each Den release,
2186
+ because `claude` can't list its models. The permission mode is
2187
+ `--permission-mode` (`default`, `acceptEdits`, `bypassPermissions`), and
2188
+ each command prefix becomes two allow rules, `Bash(<prefix>)` and
2189
+ `Bash(<prefix> *)`, in the agent's settings file beside Den's deny rules
2190
+ (deny wins). Codex CLI, Gemini CLI, Grok Build and generic take no model,
2191
+ mode or commands from Den yet: the dialog says "not supported on <runtime>"
2192
+ and the start is refused if you set one.
2193
+
2114
2194
  ### The Agents tab
2115
2195
 
2116
2196
  "Agents" in the rail (Develop, after Graph) is `/agents`: every session and
@@ -2132,15 +2212,78 @@ agent of the workspaces in view, and their terminals in the browser.
2132
2212
  it, or start it again.
2133
2213
  - **A pane** is the session's terminal: it shows the scrollback, then the
2134
2214
  live output, and your keys go to the session; its size follows the
2135
- pane. Click more rows to tile up to four (a fifth closes the oldest).
2136
- The URL holds the open panes (`/agents?open=<id>,<id>`), so a link, a
2137
- reload or Back reopens them.
2215
+ pane. Up to four are open at once (a fifth closes the oldest). The URL
2216
+ holds the open panes (`/agents?open=<id>,<id>`), so a link, a reload or
2217
+ Back reopens them. The list and the panes fill the window down to the
2218
+ runs dock.
2219
+ - **Layouts.** The switch in the page header picks one, or ⌃⇧1, ⌃⇧2, ⌃⇧3
2220
+ (Ctrl+Shift+1/2/3 on every OS; ⌘1-9 and Ctrl+1-9 stay the browser's, for
2221
+ its tabs), or ⌘K "Layout: Focus", "Layout: Tabs", "Layout: Tile". Inside
2222
+ a terminal those keys switch the layout and never reach the agent:
2223
+ - **Focus** (the default) shows one pane. A click replaces it;
2224
+ Shift-click or ⌘-click adds one alongside.
2225
+ - **Tabs** shows one pane under a strip of tabs. Each tab has the
2226
+ agent's name, then its persona and workspace ("reviewer · north"), and
2227
+ a dot when it is waiting.
2228
+ - **Tile** shows the open panes in a grid: one fills it, two sit side
2229
+ by side, three or four make a 2×2.
2230
+
2231
+ At phone width, Focus and Tile show as Tabs. The choice is yours: it is
2232
+ saved in `config/local/user.yml` as `agents.layout` (`POST /api/settings
2233
+ { agents_layout: "focus" | "tabs" | "tile" | null }`, also in Settings ›
2234
+ Agents). The URL says it too (`&layout=tabs`); a link's layout is used
2235
+ for that page and saves nothing.
2236
+
2237
+ - **Rename an agent** by double-clicking its tab: Enter saves, Esc keeps
2238
+ the old name. The name (trimmed, up to 40 characters, no control
2239
+ characters) is stored in the agent's record as `displayName`, and the
2240
+ list, its pane, ⌘K and `agents list --json` show it. The agent's id
2241
+ never changes. Clear the name to go back to the id. The route is `POST
2242
+ /api/workspaces/<ws>/agents/<id>/name` (`{ name: string | null }`), for
2243
+ a person only.
2138
2244
  - **Close** (×) detaches, as leaving the page does: the session keeps
2139
2245
  running, and opening it again replays what you missed. **Stop** (an
2140
2246
  agent only) is `agents stop`. **+ Start agent** (or
2141
2247
  `/agents?start=1`) starts one and opens its pane; under All it asks for
2142
2248
  the workspace. It says before Start whether the runtime can run, as the
2143
2249
  Workspaces page's form does. It never asks for the admin password.
2250
+ - **Shift+Enter** adds a soft newline instead of submitting. It sends the
2251
+ runtime's `softNewline` from the runtime catalog: `esc-cr` (ESC CR, the
2252
+ default; Claude Code and Gemini CLI), `lf` (Ctrl+J; Codex CLI) or `none`
2253
+ (Enter itself). A plain session's Shift+Enter is Enter. A catalog entry
2254
+ a distribution or `user.yml` adds may set `softNewline`.
2255
+ - **The session's name.** A runtime may name its conversation: Claude
2256
+ Code's `/rename` writes a `custom-title` record to its transcript, and
2257
+ Den reads the latest one (the catalog marks this source `unofficial`:
2258
+ the format is undocumented, so a name that can't be read is simply
2259
+ none). It reads on from where it last stopped, never the whole file at
2260
+ once. The most recent rename wins, Den's (a tab's) or the runtime's. The
2261
+ pane header adds ` · <name>` after the runtime, and the tab, the list and
2262
+ ⌘K add it too, when it is not empty and says more than the agent's own
2263
+ name or persona. `GET /api/sessions`, `agents list --json` and the start
2264
+ and resume answers (MCP `agent_resume`) carry it as `sessionTitle`
2265
+ (0.4.12: `sessionName`, which is the record's session handle). A
2266
+ `/rename "api spike"` shows as api spike: one pair of quotes around the
2267
+ whole name is dropped, for display only.
2268
+ Codex CLI, Gemini CLI and Grok Build have no name of their own yet. A
2269
+ catalog entry's `titleFromOsc: true` marks a runtime whose terminal
2270
+ title (OSC 0/2) names the session; no built-in runtime sets it, and the
2271
+ session host doesn't read titles yet.
2272
+ - **Links.** A URL in a pane, and an OSC 8 hyperlink, open on ⌘-click
2273
+ (Ctrl-click on Windows and Linux) in a new tab, with `noopener`. Only
2274
+ `http:` and `https:` open; an OSC 8 link with any other scheme
2275
+ (`javascript:`, `file:`) is no link. A plain click selects text as
2276
+ ever. A file path an agent prints (`src/foo.ts:42`, `./a/b.js:3:7`, an
2277
+ absolute path) is a link when it exists inside the agent's workspace
2278
+ (its directory, or the agent's own folder), resolved against the
2279
+ agent's folder; one outside it never is. The server checks with `GET
2280
+ /api/workspaces/<ws>/agents/<id>/paths?p=<path>&p=…` (stat only, nothing
2281
+ is read; at most 64 at once), which answers each path's absolute path
2282
+ or null. ⌘-click opens it in the editor of Settings › Editor & terminal ›
2283
+ "Open file links in" (`editor_open` in `config/local/user.yml` and
2284
+ `/api/settings`: `vscode`, `cursor` or `zed`, by its URL scheme at the
2285
+ line), or, with `none` (the default), copies its absolute path and says
2286
+ "Copied".
2144
2287
  - ⌘K lists each as "Agents › <ws> / <name>", and "Start agent…".
2145
2288
  - Several browsers and `<cli> sessions attach` can attach to one session
2146
2289
  at once and each sees every byte; the terminal's size is the last
@@ -2326,13 +2469,18 @@ a Claude Code agent file, read as it is, plus a few keys of Den's own:
2326
2469
  ```markdown
2327
2470
  ---
2328
2471
  name: planner # the id (or `id:`; default: the file name)
2329
- description: Writes stories and their acceptance criteria. # or `role:`
2472
+ display_name: Rita # optional; shown as "Rita (reviewer)"
2473
+ role: planner # a role (see Roles); a sentence here is the description
2474
+ category: engineering # see Categories; default: the role's
2475
+ description: Writes stories and their acceptance criteria. # one line
2330
2476
  model: some-model # optional; `inherit` means none
2477
+ version: latest # or a pinned version of the model
2478
+ allowed_commands: [npm test] # extra plain prefixes allowed without asking
2331
2479
  tools: Read, Grep # hints for the runtime; Den restricts nothing with them
2332
2480
  standing: true # starts with its workspace (later slices)
2333
2481
  context: [docs/specs, the design system] # what to read first
2334
2482
  runtime: generic # default den-claude
2335
- permission_mode: plan # den-claude only; default `default`
2483
+ permission_mode: ask # ask, edits or all (Claude Code's own modes still read)
2336
2484
  den_hooks: false # Den's guardrail hooks for this persona (see Guardrails)
2337
2485
  den_instructions: false # Den's shared instructions for this persona
2338
2486
  argv: ["some-cli", "--model", "{model}", "--mcp-config", "{mcp_config}"]
@@ -2342,8 +2490,9 @@ You are the planner. …
2342
2490
  ```
2343
2491
 
2344
2492
  Den ships a default pack: `planner` and `orchestrator` (standing), `fe-dev`,
2345
- `be-dev`, `devops`, `reviewer` and `ux-designer`. Add your own packs, read in
2346
- order after Den's; a persona with the same id replaces the earlier one:
2493
+ `be-dev` and `devops` (role `sde`), `reviewer`, `tester` and `ux-designer`
2494
+ (role `designer`, category Design). Add your own packs, read in order after
2495
+ Den's; a persona with the same id replaces the earlier one:
2347
2496
 
2348
2497
  ```yaml
2349
2498
  agents:
@@ -2356,6 +2505,53 @@ used without converting them. Files without front matter (a README) are
2356
2505
  skipped; two files with one id in a pack, or a pack that isn't a directory,
2357
2506
  are errors naming the file or the key.
2358
2507
 
2508
+ **Your own personas** live in `<stateDir>/personas/<id>.md` (0600), in the
2509
+ same format, read last: one wins over a pack's persona of the same id.
2510
+ Settings › Personas lists every persona with the Start dialog's search and
2511
+ chips: **View** (or **Edit** for your own), **Duplicate**, **Delete** (your
2512
+ own only) and **+ New persona**. A built-in or distribution persona is
2513
+ read-only; Duplicate makes your own copy to edit. "newer available" marks a
2514
+ persona pinned to an older version, with **Use latest** for your own. The
2515
+ routes: `GET /api/personas/<id>` (with its instructions), `POST
2516
+ /api/personas` (new), `POST /api/personas/<id>` (edit; 409 `read_only` for
2517
+ one that isn't yours) and `DELETE /api/personas/<id>`. Each field is
2518
+ checked: the role, category, runtime, model and version must be registered,
2519
+ the description one line of at most 200 characters, the name at most 40,
2520
+ the instructions at most 20000 characters with no control characters but
2521
+ newlines and tabs. An agent's token can't create, change or delete a
2522
+ persona (403); a person sets what agents may do.
2523
+
2524
+ **Roles.** A role is a template and a key: default instructions, a
2525
+ permission mode and a command preset (read-only commands, allowed without
2526
+ asking). Den's roles are `planner`, `orchestrator`, `sde`, `reviewer`,
2527
+ `tester` and `designer`. Picking a role in New persona fills the form in,
2528
+ never over text you typed; a role adds nothing hidden to an agent's prompt.
2529
+ A distribution adds roles, and may extend a Den role's preset but not
2530
+ redefine it:
2531
+
2532
+ ```yaml
2533
+ agents:
2534
+ roles:
2535
+ copywriter:
2536
+ description: Drafts release notes and site copy.
2537
+ instructions: Draft copy in the house voice. Leave drafts for review.
2538
+ mode: ask # ask, edits or all
2539
+ preset: [vale]
2540
+ category: marketing
2541
+ reviewer:
2542
+ preset: [semgrep] # added to Den's preset
2543
+ ```
2544
+
2545
+ **Categories** group personas in the Start list and Settings › Personas.
2546
+ Den's are Engineering, Design, Product, Marketing and Operations; a
2547
+ distribution adds more:
2548
+
2549
+ ```yaml
2550
+ agents:
2551
+ categories:
2552
+ - { id: legal, label: Legal }
2553
+ ```
2554
+
2359
2555
  ### Guardrails: hooks, shared instructions, hand-backs
2360
2556
 
2361
2557
  Every persona in Den's default pack gets two things with no setup: Den's
@@ -2626,6 +2822,10 @@ agents:
2626
2822
  dirs: [~/.local/bin, $AIDER_DIR, "{system}"]
2627
2823
  launch:
2628
2824
  argv: ["{bin}"] # "{bin}" first: the path found
2825
+ # Optional, how it behaves in an Agents tab pane (AG-4b):
2826
+ softNewline: esc-cr # what Shift+Enter sends: esc-cr (default), lf, none
2827
+ titleFromOsc: false # its terminal title (OSC 0/2) names the session
2828
+ # sessionName: official | unofficial (built-in adapters only)
2629
2829
  ```
2630
2830
 
2631
2831
  **Where a runtime's CLI is found.** As your terminal would find it, not
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monster-orange/den",
3
- "version": "0.4.11",
3
+ "version": "0.4.13",
4
4
  "description": "Den: a local multi-repo dev orchestrator, HTTPS edge and E2E harness",
5
5
  "type": "module",
6
6
  "bin": {
@@ -64,6 +64,7 @@
64
64
  "@playwright/test": "^1.61.1",
65
65
  "@slack/webhook": "^7.0.2",
66
66
  "@xterm/addon-fit": "0.11.0",
67
+ "@xterm/addon-web-links": "0.12.0",
67
68
  "@xterm/xterm": "6.0.0",
68
69
  "ajv": "^8.12.0",
69
70
  "ajv-formats": "^2.1.1",
@@ -17,6 +17,18 @@
17
17
  * /usr/local/bin and npm's global bin)
18
18
  * launch argv: its interactive CLI, "{bin}" first (the path found)
19
19
  *
20
+ * And, optional, how it behaves in a terminal pane (AG-4b):
21
+ *
22
+ * softNewline what Shift+Enter sends in an Agents tab pane: "esc-cr"
23
+ * (ESC CR, the default), "lf" (Ctrl+J) or "none" (Enter
24
+ * itself: Shift+Enter submits)
25
+ * sessionName where the runtime adapter reads a session's name from:
26
+ * "official" (a documented interface) or "unofficial" (an
27
+ * undocumented file format, read defensively). None: the
28
+ * runtime has no name of its own; Den's rename still works.
29
+ * titleFromOsc the terminal title (OSC 0/2) is a session name. Off by
30
+ * default: many CLIs put a spinner or the cwd there.
31
+ *
20
32
  * Each built-in entry was checked against the vendor's own docs or
21
33
  * installer (the URLs beside it). A vendor with no official CLI is left
22
34
  * out (docs/distributions.md, Runtimes, says which, and why); nothing is
@@ -38,6 +50,19 @@ export interface CatalogIdentity {
38
50
  realpath_under?: string[];
39
51
  }
40
52
 
53
+ /** What Shift+Enter sends in a pane (AG-4b). */
54
+ export const SOFT_NEWLINES = ["esc-cr", "lf", "none"] as const;
55
+ export type SoftNewline = (typeof SOFT_NEWLINES)[number];
56
+ const SOFT_NEWLINE_SEQ: Record<SoftNewline, string> = {
57
+ "esc-cr": "\x1b\r",
58
+ lf: "\n",
59
+ // none: Shift+Enter is Enter.
60
+ none: "\r",
61
+ };
62
+ /** How stable a runtime's session-name source is (AG-4b). */
63
+ export const SESSION_NAME_SOURCES = ["official", "unofficial"] as const;
64
+ export type SessionNameSource = (typeof SESSION_NAME_SOURCES)[number];
65
+
41
66
  export interface CatalogEntry {
42
67
  id: string;
43
68
  vendor: string;
@@ -46,6 +71,19 @@ export interface CatalogEntry {
46
71
  version: { args: string[]; identity: CatalogIdentity };
47
72
  dirs: string[];
48
73
  launch: { argv: string[] };
74
+ /** What Shift+Enter sends (AG-4b); unset: "esc-cr". */
75
+ softNewline?: SoftNewline;
76
+ /** The adapter reads a session name, from a source this stable. */
77
+ sessionName?: SessionNameSource;
78
+ /** The terminal title (OSC 0/2) names the session. Unset: no. */
79
+ titleFromOsc?: boolean;
80
+ }
81
+
82
+ /** The bytes Shift+Enter sends for an entry (ESC CR when it says none). */
83
+ export function softNewlineSeq(
84
+ e: Pick<CatalogEntry, "softNewline"> | undefined,
85
+ ): string {
86
+ return SOFT_NEWLINE_SEQ[e?.softNewline ?? "esc-cr"];
49
87
  }
50
88
 
51
89
  /** The keys an entry takes (the validator refuses any other). */
@@ -57,6 +95,9 @@ const ENTRY_KEYS = [
57
95
  "version",
58
96
  "dirs",
59
97
  "launch",
98
+ "softNewline",
99
+ "sessionName",
100
+ "titleFromOsc",
60
101
  ] as const;
61
102
 
62
103
  /** Den's built-in catalog, in the order lists show them. */
@@ -80,6 +121,12 @@ export const BUILTIN_CATALOG: readonly CatalogEntry[] = [
80
121
  },
81
122
  dirs: ["~/.local/bin", "~/.claude/local", "{system}"],
82
123
  launch: { argv: ["{bin}"] },
124
+ // https://code.claude.com/docs/en/terminal-config: Option+Enter (Meta,
125
+ // ESC CR) is a newline; /terminal-setup binds Shift+Enter to it.
126
+ softNewline: "esc-cr",
127
+ // `/rename` appends {"type":"custom-title","customTitle":…} to the
128
+ // session's transcript (<token>.jsonl). Undocumented: read defensively.
129
+ sessionName: "unofficial",
83
130
  },
84
131
  // Codex CLI. https://github.com/openai/codex: binary `codex`, npm
85
132
  // @openai/codex, Homebrew cask codex. The standalone installer
@@ -98,6 +145,9 @@ export const BUILTIN_CATALOG: readonly CatalogEntry[] = [
98
145
  },
99
146
  dirs: ["$CODEX_INSTALL_DIR", "~/.local/bin", "{system}"],
100
147
  launch: { argv: ["{bin}"] },
148
+ // The composer's insert_newline is Ctrl+J (codex-rs/tui keymap): a
149
+ // plain terminal's Shift+Enter and Alt+Enter submit (openai/codex#20580).
150
+ softNewline: "lf",
101
151
  },
102
152
  // Gemini CLI. https://github.com/google-gemini/gemini-cli: binary
103
153
  // `gemini`, npm @google/gemini-cli, Homebrew formula gemini-cli;
@@ -115,6 +165,9 @@ export const BUILTIN_CATALOG: readonly CatalogEntry[] = [
115
165
  },
116
166
  dirs: ["{system}"],
117
167
  launch: { argv: ["{bin}"] },
168
+ // https://geminicli.com/docs/reference/keyboard-shortcuts: Alt+Enter
169
+ // (ESC CR), Ctrl+J and Shift+Enter insert a newline.
170
+ softNewline: "esc-cr",
118
171
  },
119
172
  // Grok Build. https://docs.x.ai/build/overview: install with
120
173
  // https://x.ai/cli/install.sh, run `grok` in a project directory. The
@@ -279,6 +332,29 @@ export function validateCatalogEntry(
279
332
  throw new Error(
280
333
  `${where}.launch.argv[0]: must be "{bin}" (the path Den found)`,
281
334
  );
335
+ const traits: Pick<
336
+ CatalogEntry,
337
+ "softNewline" | "sessionName" | "titleFromOsc"
338
+ > = {};
339
+ if (raw.softNewline !== undefined) {
340
+ if (!(SOFT_NEWLINES as readonly unknown[]).includes(raw.softNewline))
341
+ throw new Error(
342
+ `${where}.softNewline: ${shown(raw.softNewline)} must be one of ${SOFT_NEWLINES.join(", ")}`,
343
+ );
344
+ traits.softNewline = raw.softNewline as SoftNewline;
345
+ }
346
+ if (raw.sessionName !== undefined) {
347
+ if (!(SESSION_NAME_SOURCES as readonly unknown[]).includes(raw.sessionName))
348
+ throw new Error(
349
+ `${where}.sessionName: ${shown(raw.sessionName)} must be one of ${SESSION_NAME_SOURCES.join(", ")}`,
350
+ );
351
+ traits.sessionName = raw.sessionName as SessionNameSource;
352
+ }
353
+ if (raw.titleFromOsc !== undefined) {
354
+ if (typeof raw.titleFromOsc !== "boolean")
355
+ throw new Error(`${where}.titleFromOsc: must be true or false`);
356
+ traits.titleFromOsc = raw.titleFromOsc;
357
+ }
282
358
  return {
283
359
  id,
284
360
  vendor: text(`${where}.vendor`, raw.vendor),
@@ -287,6 +363,7 @@ export function validateCatalogEntry(
287
363
  version: { args, identity },
288
364
  dirs,
289
365
  launch: { argv },
366
+ ...traits,
290
367
  };
291
368
  }
292
369