@monster-orange/den 0.4.15 → 0.4.17
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/CHANGELOG.md +125 -0
- package/README.md +7 -7
- package/docs/distributions.md +137 -11
- package/package.json +1 -1
- package/src/shared/agents/conversations.ts +122 -4
- package/src/shared/agents/den-claude.ts +13 -1
- package/src/shared/agents/records.ts +19 -0
- package/src/shared/agents/runtime.ts +5 -0
- package/src/shared/github-app.ts +7 -0
- package/src/ui/cli/agent-ops.ts +304 -1
- package/src/ui/cli/agents-command.ts +71 -7
- package/src/ui/server/agent-routes.ts +92 -2
- package/src/ui/server/check-pool.ts +72 -0
- package/src/ui/server/github-routes.ts +1 -1
- package/src/ui/server/index.ts +127 -15
- package/src/ui/server/public/agents-page.js +358 -38
- package/src/ui/server/public/agents.html +10 -0
- package/src/ui/server/public/environments.html +223 -140
- package/src/ui/server/public/error-page.js +70 -15
- package/src/ui/server/public/error.html +18 -0
- package/src/ui/server/public/graph.css +18 -0
- package/src/ui/server/public/graph.js +23 -6
- package/src/ui/server/public/i18n/es.js +47 -5
- package/src/ui/server/public/i18n/ja.js +47 -5
- package/src/ui/server/public/index.html +6 -0
- package/src/ui/server/public/local-dev.html +188 -25
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/public/persona-ui.css +11 -0
- package/src/ui/server/public/release.html +19 -1
- package/src/ui/server/public/shell.js +187 -12
- package/src/ui/server/public/topbar.js +34 -1
- package/src/ui/server/public/view-scope.js +61 -0
- package/src/ui/server/public/workspace-picker.js +2 -0
- package/src/ui/server/route-classes.ts +8 -0
- package/src/ui/server/run-history.ts +181 -0
- package/src/ui/server/session-routes.ts +97 -17
- package/src/ui/server/ui-assets.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,131 @@ 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.17] - 2026-11-08
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **An agent's workspace is a link.** The agent pane's header and the
|
|
12
|
+
workspace's heading in the Agents list link to the Workspaces page
|
|
13
|
+
focused on it (`?ws=`). ⌘K has "Go to workspace <ws>" for every
|
|
14
|
+
workspace, the front agent's first on the Agents page, found by the
|
|
15
|
+
workspace's name or any of its agents'.
|
|
16
|
+
- **"Last test run" is the server's.** The Workspaces page's "Last run"
|
|
17
|
+
column is now "Last test run", read from the server's run history
|
|
18
|
+
(`<state>/run-history.jsonl`, `GET /api/runs/last`). It shows runs from
|
|
19
|
+
every browser and survives a restart. The service's own state ("up 2h")
|
|
20
|
+
is shown apart from it.
|
|
21
|
+
- **One org rule.** Environments and Releases have no org picker of their
|
|
22
|
+
own. Both follow the top bar's View and say so with the same note. A
|
|
23
|
+
page shows its own org picker only when it filters by org.
|
|
24
|
+
- **Environments never waits on a slow host.** Every row shows at once,
|
|
25
|
+
pulsing "checking", and each result streams into its row. The server
|
|
26
|
+
runs at most 12 checks at once, each capped at 10 s; one that runs out
|
|
27
|
+
of time says "timed out after 10 s". Re-check works while checks run.
|
|
28
|
+
- **An agent in an inactive workspace opens its terminal.** The terminal
|
|
29
|
+
route checks the person and the agent, not the workspace's active
|
|
30
|
+
flag. The pane says "Sign in" only when you're signed out; any other
|
|
31
|
+
refusal gives its real reason.
|
|
32
|
+
- **Menus are never clipped.** Every ⋯ menu in the shell (Workspaces
|
|
33
|
+
rows and the workspace menu, the top bar's menus) now opens in the
|
|
34
|
+
browser's top layer through one shared helper, so a scrolling table or
|
|
35
|
+
card no longer cuts it off. It stays beside its button, flips up near
|
|
36
|
+
the bottom of the window, and closes on an outside click as before.
|
|
37
|
+
- **The Tests module picker is never clipped.** It uses the same
|
|
38
|
+
top-layer menu.
|
|
39
|
+
- **Visit ↗ opens a service.** Workspaces (beside Start, Stop, Restart
|
|
40
|
+
and Logs, and as a link on the service's name), Services and Graph
|
|
41
|
+
cards open the service's edge URL when it is routed, else its local
|
|
42
|
+
URL, in a new tab. A service with no URL has no Visit. Below 1600 px
|
|
43
|
+
wide the Workspaces button shows only "↗". The detail panel's "Open ↗"
|
|
44
|
+
is now "Visit ↗".
|
|
45
|
+
- **Favourite a service from Workspaces and Graph.** Workspaces rows and
|
|
46
|
+
Graph cards (top-right) have the ☆ star. It uses the same list as
|
|
47
|
+
Services, so a favourite set on one page shows on all of them.
|
|
48
|
+
- **Change persona on an agent that was never prompted** said "same
|
|
49
|
+
conversation", though there was nothing to resume: it starts fresh under
|
|
50
|
+
the same conversation id, as Resume does. The CLI, the API (`note`, with
|
|
51
|
+
`fresh: true`), the toast and the pane now say "nothing to resume yet;
|
|
52
|
+
started fresh (same conversation id, folder and scope)".
|
|
53
|
+
- **Last test run counts a repo's own suites.** A run of the suites pillar
|
|
54
|
+
(a repo's `tests:`, such as `unit`) and a repo's performance modules
|
|
55
|
+
now name the repos they test, so a repo with only `tests: unit` fills
|
|
56
|
+
its Last test run. Den's own integration tests name none.
|
|
57
|
+
- **The Workspaces table keeps its height in a short window.** Under about
|
|
58
|
+
620 px tall it shrank to nothing and its rows and ⋯ were out of reach.
|
|
59
|
+
It keeps at least 200 px and scrolls.
|
|
60
|
+
|
|
61
|
+
### Migrations
|
|
62
|
+
|
|
63
|
+
None.
|
|
64
|
+
|
|
65
|
+
## [0.4.16] - 2026-11-08
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- **Change an agent's persona (AG-18).** **Change persona…** in the
|
|
70
|
+
agent pane's ⋯ menu (running or stopped), and `<cli> agents persona
|
|
71
|
+
<id> <persona> [--model <m>] [--now | --after-turn]`. Den stops the
|
|
72
|
+
session and resumes the same conversation with the new persona's role
|
|
73
|
+
prompt, permission mode and allowed commands; the history, id, name,
|
|
74
|
+
workspace, folder, tab and the model it runs stay. The dialog marks the
|
|
75
|
+
current persona and lists what changes. Mid-turn it never stops a turn
|
|
76
|
+
unasked: "It's working. Change after this turn, or stop it now?" (409
|
|
77
|
+
`busy`); After this turn waits for the turn to end, Stop it now stops
|
|
78
|
+
it. The wait lives in the server's memory: if Den restarts first, the
|
|
79
|
+
change is dropped, the record no longer marks it pending, and the audit
|
|
80
|
+
trail says "persona change cancelled: Den restarted". A person's only: an agent's token is refused (403), and there is no
|
|
81
|
+
MCP tool. Only a runtime that can resume offers it. The record keeps
|
|
82
|
+
`personaChanges`, and the audit trail records each change. Route `POST
|
|
83
|
+
/api/workspaces/<ws>/agents/<id>/persona`.
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
- **Bring in keeps the conversation's model, and says what the persona
|
|
88
|
+
changes (AG-17.1c).** It used to switch the conversation to the
|
|
89
|
+
persona's model, and drop MCP servers the conversation had outside Den,
|
|
90
|
+
without a word. Now the model stays the one its last answer came from
|
|
91
|
+
("Keeps <model> from the conversation"; Advanced or `--model` picks
|
|
92
|
+
another), and a short list under the persona says "From now on it works
|
|
93
|
+
as <persona>: its role, its permissions (<mode>), Den's tools and
|
|
94
|
+
guards. Its history stays.", plus "It won't have: <servers>." for MCP
|
|
95
|
+
servers seen in its transcript that Den won't load. The persona of the
|
|
96
|
+
latest agent that worked in its folder is preselected (`suggest`), never
|
|
97
|
+
submitted. The list adds `model`, `lost` and `suggest`; the import takes
|
|
98
|
+
`model` and `version`.
|
|
99
|
+
- **The signed-out page shows Sign in with GitHub once the owner is
|
|
100
|
+
linked, with no reload.** With no owner yet it watches the session
|
|
101
|
+
probe (which now says `github.state` to a signed-out browser), and step 1
|
|
102
|
+
turns done and the button appears as soon as `web owner link` finishes.
|
|
103
|
+
|
|
104
|
+
- **The signed-out page explains Sign in with GitHub (SEC-2a-UX).** It
|
|
105
|
+
shows the same three steps whether or not an owner is linked: on the
|
|
106
|
+
machine Den runs on, run `<cli> web owner link` (or use Settings ›
|
|
107
|
+
Access there); enter the code it shows at github.com/login/device and
|
|
108
|
+
approve; come back and press Sign in with GitHub with the same account.
|
|
109
|
+
Once an owner is linked, step 1 shows as done ("Owner: @login") and the
|
|
110
|
+
button follows the steps. Below them: only the owner's GitHub account
|
|
111
|
+
can sign in this way, anyone else uses Approve this computer, and Learn
|
|
112
|
+
more opens docs/distributions.md, "Signing in with GitHub". English,
|
|
113
|
+
Spanish and Japanese.
|
|
114
|
+
- Another GitHub account is refused with "@login isn't the owner of this
|
|
115
|
+
Den. Only the owner's account can use Sign in with GitHub. Use Approve
|
|
116
|
+
this computer instead." It no longer says to ask the owner to add you:
|
|
117
|
+
a Den has one owner, and no one to add.
|
|
118
|
+
|
|
119
|
+
### Fixed
|
|
120
|
+
|
|
121
|
+
- An open agent pane stayed on the old session after Change persona, Resume
|
|
122
|
+
or a rotation, showing "[the session exited (143)]" while the list showed
|
|
123
|
+
the new one running. A pane now follows its agent: it moves to the new
|
|
124
|
+
session in the same slot, keeps its scrollback, and says "Switched to
|
|
125
|
+
<persona>". After a session ends the page looks again every second for
|
|
126
|
+
15 s, so the switch shows within about 2 s.
|
|
127
|
+
|
|
128
|
+
### Migrations
|
|
129
|
+
|
|
130
|
+
- None.
|
|
131
|
+
|
|
7
132
|
## [0.4.15] - 2026-11-07
|
|
8
133
|
|
|
9
134
|
### Added
|
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.
|
|
14
|
-
<img src="docs/images/0.4.
|
|
13
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.17/hero-dark.png">
|
|
14
|
+
<img src="docs/images/0.4.17/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.
|
|
41
|
+
<img src="docs/images/0.4.17/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.
|
|
72
|
-
<img src="docs/images/0.4.
|
|
73
|
-
<img src="docs/images/0.4.
|
|
74
|
-
<img src="docs/images/0.4.
|
|
71
|
+
<img src="docs/images/0.4.17/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.17/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.17/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.17/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
|
package/docs/distributions.md
CHANGED
|
@@ -355,6 +355,19 @@ is never mistaken for another repo's service. An alias that names more than
|
|
|
355
355
|
one service is an error that lists them (`"worker" is ambiguous in workspace
|
|
356
356
|
"default": one/worker, two/worker`); name one of them.
|
|
357
357
|
|
|
358
|
+
## Opening a service
|
|
359
|
+
|
|
360
|
+
A service with a URL gets **Visit ↗**, which opens it in a new tab. It
|
|
361
|
+
shows on Workspaces (beside Start, Stop, Restart and Logs, and as a link
|
|
362
|
+
on the service's name), on Services and on Graph cards. Den picks the
|
|
363
|
+
URL in this order: the service's `urls.<env>`; for `local`, its edge
|
|
364
|
+
route when the edge serves one; else the origin of its HTTP health
|
|
365
|
+
check. A service with none of these has no Visit.
|
|
366
|
+
|
|
367
|
+
The ☆ star on Workspaces rows, Services and Graph cards marks a
|
|
368
|
+
favourite. It is one list, saved in `shell.pins`, so a star set on one
|
|
369
|
+
page shows on all of them.
|
|
370
|
+
|
|
358
371
|
## Health checks
|
|
359
372
|
|
|
360
373
|
A service declares its health per environment under `health`, in one of
|
|
@@ -432,6 +445,36 @@ that a container existed, running or not).
|
|
|
432
445
|
`service`, with both forms at once, or with a key of another type (`cmd`,
|
|
433
446
|
`url`), and says which.
|
|
434
447
|
|
|
448
|
+
### The Environments page
|
|
449
|
+
|
|
450
|
+
The Environments page lists every service's row at once, each cell pulsing
|
|
451
|
+
"checking" until its result arrives. The results stream in one at a time
|
|
452
|
+
(`GET /api/health/stream`, one event per check), so a slow host holds up
|
|
453
|
+
its own cell only. The server runs at most 12 checks at once, and no check
|
|
454
|
+
takes longer than 10 s. A check that runs out of time says "timed out after
|
|
455
|
+
10 s" in its row. Re-check and the page's other controls work while the
|
|
456
|
+
checks run.
|
|
457
|
+
|
|
458
|
+
The page has no org picker of its own. It follows the top bar's View (all
|
|
459
|
+
active workspaces, or one), asks for that workspace only, and says so:
|
|
460
|
+
"Shows workspace south, as the top bar's View. Change it there." The
|
|
461
|
+
Release page follows the same rule with the same note. A page shows its own
|
|
462
|
+
org picker only when it filters by org.
|
|
463
|
+
|
|
464
|
+
### Last test run
|
|
465
|
+
|
|
466
|
+
The Workspaces page's "Last test run" column shows each repo's latest test
|
|
467
|
+
run: its result (✓ or ✗), how long ago, and the pillar and environment on
|
|
468
|
+
hover. It reads the server's run history, so it shows runs started from any
|
|
469
|
+
browser or tab, and it survives a restart. The service's own state ("up 2h")
|
|
470
|
+
is shown apart from it, in the Health cell.
|
|
471
|
+
|
|
472
|
+
The history is `<state>/run-history.jsonl` (mode 0600): one line when a run
|
|
473
|
+
starts and one when it ends. Past 2000 lines it keeps the newest 1000.
|
|
474
|
+
`GET /api/runs/last` (read only) gives `{ repos: { <repo>: { id, pillar,
|
|
475
|
+
env, repos, startedAt, endedAt, exitCode, running } } }`. A run a restart
|
|
476
|
+
cut off has no end and is not shown.
|
|
477
|
+
|
|
435
478
|
## Placement
|
|
436
479
|
|
|
437
480
|
Each service says where it runs, per environment: `local` (on this machine,
|
|
@@ -1687,14 +1730,31 @@ the account's id and login, never a token, and no other account gets in.
|
|
|
1687
1730
|
`web owner unlink`. Settings › Access does the same in the browser
|
|
1688
1731
|
(Link your GitHub account, Unlink): shell-grade actions, for a signed-in
|
|
1689
1732
|
browser and never an agent's token.
|
|
1690
|
-
- **Sign in with GitHub** on the signed-out page shows
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1733
|
+
- **Sign in with GitHub** on the signed-out page shows the same three
|
|
1734
|
+
steps whether or not an owner is linked:
|
|
1735
|
+
1. On the machine Den runs on, run `web owner link`, or use
|
|
1736
|
+
Settings › Access there.
|
|
1737
|
+
2. It shows a code. Enter it at `github.com/login/device` and approve.
|
|
1738
|
+
3. Come back to the signed-out page and press **Sign in with GitHub**,
|
|
1739
|
+
with the same GitHub account.
|
|
1740
|
+
|
|
1741
|
+
Once an owner is linked, step 1 shows as done ("Owner: @<login>") and
|
|
1742
|
+
the button follows the steps; before that there is no button. Below the
|
|
1743
|
+
steps the page says only the owner's GitHub account can sign in this
|
|
1744
|
+
way, and that anyone else uses **Approve this computer**; **Learn more**
|
|
1745
|
+
opens this section. With no owner yet, the page watches (the session
|
|
1746
|
+
probe, `GET /api/session`, says `github.state` to a signed-out
|
|
1747
|
+
browser): once the owner is linked on the machine, step 1 shows done
|
|
1748
|
+
and the button appears, with no reload.
|
|
1749
|
+
|
|
1750
|
+
- Pressing the button shows a code and `github.com/login/device`. The
|
|
1751
|
+
server runs the flow, and when the owner approves it sets the same
|
|
1752
|
+
never-expiring session cookie a one-time link sets, and the page reloads
|
|
1753
|
+
signed in. Another account sees "@<login> isn't the owner of this Den.
|
|
1754
|
+
Only the owner's account can use Sign in with GitHub. Use Approve this
|
|
1755
|
+
computer instead." Den has one owner, so there is no one to add. The
|
|
1756
|
+
token is thrown away either way. At most three sign-ins wait for GitHub
|
|
1757
|
+
at once; a code expires as GitHub says (15 minutes).
|
|
1698
1758
|
- **Settings › Access** lists the linked devices (the browser, how it
|
|
1699
1759
|
signed in, the client address when the edge vouched for one, created and
|
|
1700
1760
|
last seen; "This device" on yours), each with **Revoke**, which ends its session at once (its
|
|
@@ -2180,7 +2240,8 @@ server scoped to its workspace.
|
|
|
2180
2240
|
<cli> agents stop <id> [-w <ws>]
|
|
2181
2241
|
<cli> agents resume <id> [-w <ws>]
|
|
2182
2242
|
<cli> agents import -w <ws> [--runtime <r>] [--json] # conversations to bring in
|
|
2183
|
-
<cli> agents import -w <ws> --session <id> --persona <p>
|
|
2243
|
+
<cli> agents import -w <ws> --session <id> --persona <p> [--model <m>] # bring one in
|
|
2244
|
+
<cli> agents persona <id> <persona> [-w <ws>] [--model <m>] [--now | --after-turn] # change its persona
|
|
2184
2245
|
<cli> agents personas [--json]
|
|
2185
2246
|
<cli> agents doctor [-w <ws>] [--json] # can each runtime run here, and why not
|
|
2186
2247
|
<cli> agents handback-check <file|-> # is a hand-back complete and plain (see Guardrails)
|
|
@@ -2251,6 +2312,24 @@ import -w <ws>` lists them too.
|
|
|
2251
2312
|
Den's token, MCP config, hooks, guards and audit trail apply. It is an
|
|
2252
2313
|
agent like any other: stop, resume, rename. The record notes
|
|
2253
2314
|
`broughtInAt`.
|
|
2315
|
+
- **What the persona changes.** The persona is the role it takes on from
|
|
2316
|
+
now on: its role prompt, its permission mode and allowed commands, and
|
|
2317
|
+
Den's tools and guards. The history stays. The dialog says so under the
|
|
2318
|
+
persona list: "From now on it works as <persona>: its role, its
|
|
2319
|
+
permissions (<mode>), Den's tools and guards. Its history stays."
|
|
2320
|
+
- **The model stays the conversation's own.** Den reads the model of its
|
|
2321
|
+
last answer (`message.model` on the last assistant record, in the ends
|
|
2322
|
+
it reads) and runs it on that: "Keeps <model> from the conversation".
|
|
2323
|
+
Advanced (or `--model`) picks another. With no model in the
|
|
2324
|
+
transcript, the persona's applies.
|
|
2325
|
+
- **The MCP servers it won't have.** An agent runs with Den's MCP only
|
|
2326
|
+
(`--strict-mcp-config`). When the transcript shows tool calls from other
|
|
2327
|
+
servers (`mcp__<server>__…`), the dialog names them: "It won't have:
|
|
2328
|
+
<servers>." The list's `lost` field has them.
|
|
2329
|
+
- **A suggested persona.** The list's `suggest` is the persona of the
|
|
2330
|
+
latest agent of the workspace that worked in the conversation's folder
|
|
2331
|
+
(or a folder inside it, or one containing it), else the workspace's
|
|
2332
|
+
latest agent's. The dialog preselects it; you confirm with Bring in.
|
|
2254
2333
|
- **One writer only.** A conversation written in the last minute is taken
|
|
2255
2334
|
to be still open in another terminal: "Still open in another terminal.
|
|
2256
2335
|
Close it there first" (HTTP 409 `open_elsewhere`). A conversation an
|
|
@@ -2260,8 +2339,43 @@ import -w <ws>` lists them too.
|
|
|
2260
2339
|
- Only den-claude can bring one in for now. Other runtimes say "<runtime>
|
|
2261
2340
|
can't bring in a conversation yet".
|
|
2262
2341
|
- Routes: `GET /api/workspaces/<ws>/conversations[?runtime=<r>]` and `POST
|
|
2263
|
-
/api/workspaces/<ws>/conversations/<id>/import` `{ persona, runtime?, id
|
|
2264
|
-
}`. A person's only, unless MCP `agent_import` is opted
|
|
2342
|
+
/api/workspaces/<ws>/conversations/<id>/import` `{ persona, runtime?, id?,
|
|
2343
|
+
model?, version? }`. A person's only, unless MCP `agent_import` is opted
|
|
2344
|
+
in.
|
|
2345
|
+
|
|
2346
|
+
#### Change an agent's persona
|
|
2347
|
+
|
|
2348
|
+
**Change persona…** (the agent pane's ⋯ menu, running or stopped) and
|
|
2349
|
+
`agents persona <id> <persona>` switch an agent to another persona on the
|
|
2350
|
+
same conversation. Den stops its session and resumes the conversation with
|
|
2351
|
+
the new persona's role prompt, permission mode and allowed commands (the
|
|
2352
|
+
old persona's mode and commands go). The history stays, and so do the
|
|
2353
|
+
agent's id, name, workspace, folder, grants, tab and slot, and the model it
|
|
2354
|
+
runs unless Advanced (`--model`) picks another. The dialog lists the
|
|
2355
|
+
personas with the current one marked, and the same "From now on it works
|
|
2356
|
+
as…" line as Bring in.
|
|
2357
|
+
|
|
2358
|
+
- **Never mid-turn without asking.** While it works (den-claude: its
|
|
2359
|
+
transcript's last record is a tool call, or a prompt or tool result with
|
|
2360
|
+
no answer yet), the change is refused: "It's working. Change after this
|
|
2361
|
+
turn, or stop it now?" (HTTP 409 `busy`). **After this turn**
|
|
2362
|
+
(`--after-turn`, `when: "after-turn"`) waits for the turn to end, up to
|
|
2363
|
+
30 minutes, and then switches (the API answers 202 `pending` at once;
|
|
2364
|
+
the record marks it `pendingPersona` while it waits). The wait lives in
|
|
2365
|
+
the server's memory: if Den restarts first, the change is dropped, the
|
|
2366
|
+
mark is cleared at start, and the audit trail records "persona change
|
|
2367
|
+
cancelled: Den restarted";
|
|
2368
|
+
**Stop it now** (`--now`, `when: "now"`) stops it and switches.
|
|
2369
|
+
- **A person's only.** An agent could widen its own permissions with it,
|
|
2370
|
+
so an agent's token is refused (403), the CLI refuses inside an agent's
|
|
2371
|
+
session, and there is no MCP tool.
|
|
2372
|
+
- Only a runtime that can resume offers it (`personaChange: true` on the
|
|
2373
|
+
agent's row); others can't.
|
|
2374
|
+
- The record keeps each change in `personaChanges` (from, to, when, and
|
|
2375
|
+
who), and the audit trail records "persona <id> in <ws>: <from> → <to>,
|
|
2376
|
+
by <who>".
|
|
2377
|
+
- Route: `POST /api/workspaces/<ws>/agents/<id>/persona` `{ persona, model?,
|
|
2378
|
+
version?, when? }`.
|
|
2265
2379
|
|
|
2266
2380
|
### On the Workspaces page
|
|
2267
2381
|
|
|
@@ -2481,6 +2595,18 @@ text frame `{"type":"resize","cols":c,"rows":r}` resizes, and the server
|
|
|
2481
2595
|
sends `{"type":"session",…}` first and `{"type":"exit","exitCode":n}` when
|
|
2482
2596
|
the session ends.
|
|
2483
2597
|
|
|
2598
|
+
The terminal route checks the person and the agent, not whether the
|
|
2599
|
+
agent's workspace is active: an agent in a workspace you've switched away
|
|
2600
|
+
from still opens. A `GET` of the io path (not a WebSocket) says why one
|
|
2601
|
+
would be refused: `401` when you're signed out (the pane then says "Sign
|
|
2602
|
+
in"), `404` with the reason when there is no such session here, and `426`
|
|
2603
|
+
when the upgrade would be allowed. The pane shows that reason, not "Sign
|
|
2604
|
+
in", for any refusal but the first.
|
|
2605
|
+
|
|
2606
|
+
The pane's header and the workspace's heading in the list link to the
|
|
2607
|
+
Workspaces page focused on that workspace (`/local-dev?ws=<ws>`), and ⌘K
|
|
2608
|
+
has "Go to workspace <ws>" for each workspace with agents.
|
|
2609
|
+
|
|
2484
2610
|
### Stopped agents: Resume, Start fresh, Remove
|
|
2485
2611
|
|
|
2486
2612
|
An agent that stops stays in the list, below the running ones (AG-14, the
|
package/package.json
CHANGED
|
@@ -32,6 +32,16 @@ export interface RuntimeConversation {
|
|
|
32
32
|
lastAt: string;
|
|
33
33
|
/** The folder it ran in. */
|
|
34
34
|
cwd: string;
|
|
35
|
+
/**
|
|
36
|
+
* AG-17.1c: the model its last answer came from (the last assistant
|
|
37
|
+
* record's `message.model`), so Bring in keeps it. Absent when unknown.
|
|
38
|
+
*/
|
|
39
|
+
model?: string;
|
|
40
|
+
/**
|
|
41
|
+
* AG-17.1c: the MCP servers its tools came from (`mcp__<server>__<tool>`
|
|
42
|
+
* tool calls in the ends read), sorted. Absent when none.
|
|
43
|
+
*/
|
|
44
|
+
mcp?: string[];
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
/** Bytes read at each end of a transcript. */
|
|
@@ -114,22 +124,63 @@ function promptText(rec: Record<string, unknown>): string | null {
|
|
|
114
124
|
return cleanName(text);
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
/** A transcript
|
|
127
|
+
/** A model name as a transcript records it: short, printable, not synthetic. */
|
|
128
|
+
const MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$/;
|
|
129
|
+
/** An MCP tool's name: `mcp__<server>__<tool>`. */
|
|
130
|
+
const MCP_TOOL_RE = /^mcp__([A-Za-z0-9_.-]+?)__[A-Za-z0-9_.-]+$/;
|
|
131
|
+
|
|
132
|
+
/** An assistant record's tool calls' names. */
|
|
133
|
+
function toolNames(rec: Record<string, unknown>): string[] {
|
|
134
|
+
const content = (rec.message as { content?: unknown } | undefined)?.content;
|
|
135
|
+
if (!Array.isArray(content)) return [];
|
|
136
|
+
return content.flatMap((c) => {
|
|
137
|
+
const b = c as { type?: unknown; name?: unknown };
|
|
138
|
+
return b?.type === "tool_use" && typeof b.name === "string" ? [b.name] : [];
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A transcript's summary from its two ends (AG-17.1, AG-17.1c). */
|
|
143
|
+
export interface TranscriptSummary {
|
|
144
|
+
cwd: string | null;
|
|
145
|
+
title: string | null;
|
|
146
|
+
/** The last assistant record's model, or null. */
|
|
147
|
+
model: string | null;
|
|
148
|
+
/** The MCP servers its tool calls name, sorted. */
|
|
149
|
+
mcp: string[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A transcript's cwd, title, last model and MCP servers, from its two ends. */
|
|
118
153
|
export function transcriptSummary(
|
|
119
154
|
file: string,
|
|
120
155
|
bytes = ENDS_BYTES,
|
|
121
|
-
):
|
|
156
|
+
): TranscriptSummary {
|
|
122
157
|
const { head, tail } = readEnds(file, bytes);
|
|
123
158
|
let cwd: string | null = null;
|
|
124
159
|
let prompt: string | null = null;
|
|
125
160
|
let title: string | null = null;
|
|
161
|
+
let model: string | null = null;
|
|
162
|
+
const mcp = new Set<string>();
|
|
126
163
|
const titleIn = (line: string) => {
|
|
127
164
|
if (!line.includes("custom-title")) return;
|
|
128
165
|
const r = parse(line);
|
|
129
166
|
if (r?.type === "custom-title") title = cleanName(r.customTitle) ?? title;
|
|
130
167
|
};
|
|
168
|
+
// AG-17.1c: the last answer's model, and the MCP servers its tools came from.
|
|
169
|
+
const assistantIn = (line: string) => {
|
|
170
|
+
if (!line.includes('"assistant"')) return;
|
|
171
|
+
const r = parse(line);
|
|
172
|
+
if (r?.type !== "assistant") return;
|
|
173
|
+
const m = (r.message as { model?: unknown } | undefined)?.model;
|
|
174
|
+
if (typeof m === "string" && MODEL_RE.test(m) && m !== "<synthetic>")
|
|
175
|
+
model = m;
|
|
176
|
+
for (const n of toolNames(r)) {
|
|
177
|
+
const hit = MCP_TOOL_RE.exec(n);
|
|
178
|
+
if (hit) mcp.add(hit[1]);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
131
181
|
for (const line of head) {
|
|
132
182
|
titleIn(line);
|
|
183
|
+
assistantIn(line);
|
|
133
184
|
if (cwd && prompt) continue;
|
|
134
185
|
if (!line.includes('"cwd"') && !line.includes('"user"')) continue;
|
|
135
186
|
const r = parse(line);
|
|
@@ -138,8 +189,73 @@ export function transcriptSummary(
|
|
|
138
189
|
cwd = r.cwd;
|
|
139
190
|
prompt ??= promptText(r);
|
|
140
191
|
}
|
|
141
|
-
for (const line of tail)
|
|
142
|
-
|
|
192
|
+
for (const line of tail) {
|
|
193
|
+
titleIn(line);
|
|
194
|
+
assistantIn(line);
|
|
195
|
+
}
|
|
196
|
+
return { cwd, title: title ?? prompt, model, mcp: [...mcp].sort() };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Written this recently, a turn whose last record is an answer is still going. */
|
|
200
|
+
export const TURN_QUIET_MS = 5_000;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* AG-18: whether a conversation's turn is still going, from its tail: the
|
|
204
|
+
* last user or assistant record. An answer that ended (`end_turn`, a stop
|
|
205
|
+
* sequence, the token limit) is idle; a tool call, or a prompt or tool
|
|
206
|
+
* result with no answer yet, is busy; an answer with no stop reason is busy
|
|
207
|
+
* until the file has been quiet TURN_QUIET_MS. An interrupted turn is idle.
|
|
208
|
+
* No transcript: idle (nothing was asked yet).
|
|
209
|
+
*/
|
|
210
|
+
export function transcriptBusy(
|
|
211
|
+
file: string | null,
|
|
212
|
+
now = Date.now(),
|
|
213
|
+
bytes = ENDS_BYTES,
|
|
214
|
+
): boolean {
|
|
215
|
+
if (!file) return false;
|
|
216
|
+
let mtime: number;
|
|
217
|
+
try {
|
|
218
|
+
mtime = fs.statSync(file).mtimeMs;
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
const { head, tail } = readEnds(file, bytes);
|
|
223
|
+
const lines = tail.length ? [...head, ...tail] : head;
|
|
224
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
225
|
+
const line = lines[i];
|
|
226
|
+
if (!line.includes('"user"') && !line.includes('"assistant"')) continue;
|
|
227
|
+
const r = parse(line);
|
|
228
|
+
if (!r || r.isSidechain === true || r.isMeta === true) continue;
|
|
229
|
+
if (r.type === "assistant") {
|
|
230
|
+
const msg = r.message as { stop_reason?: unknown } | undefined;
|
|
231
|
+
const stop = msg?.stop_reason;
|
|
232
|
+
if (stop === "tool_use" || toolNames(r).length) return true;
|
|
233
|
+
if (
|
|
234
|
+
stop === "end_turn" ||
|
|
235
|
+
stop === "stop_sequence" ||
|
|
236
|
+
stop === "max_tokens"
|
|
237
|
+
)
|
|
238
|
+
return false;
|
|
239
|
+
return now - mtime < TURN_QUIET_MS;
|
|
240
|
+
}
|
|
241
|
+
if (r.type === "user") {
|
|
242
|
+
const content = (r.message as { content?: unknown } | undefined)?.content;
|
|
243
|
+
const text =
|
|
244
|
+
typeof content === "string"
|
|
245
|
+
? content
|
|
246
|
+
: Array.isArray(content)
|
|
247
|
+
? ((
|
|
248
|
+
content.find(
|
|
249
|
+
(c) => (c as { type?: unknown })?.type === "text",
|
|
250
|
+
) as { text?: unknown } | undefined
|
|
251
|
+
)?.text ?? "")
|
|
252
|
+
: "";
|
|
253
|
+
if (typeof text === "string" && text.startsWith("[Request interrupted"))
|
|
254
|
+
return false;
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
143
259
|
}
|
|
144
260
|
|
|
145
261
|
/** A folder's absolute forms: as given and its real path. */
|
|
@@ -236,6 +352,8 @@ export function claudeConversations(
|
|
|
236
352
|
title: s.title,
|
|
237
353
|
lastAt: new Date(f.mtimeMs).toISOString(),
|
|
238
354
|
cwd,
|
|
355
|
+
...(s.model ? { model: s.model } : {}),
|
|
356
|
+
...(s.mcp.length ? { mcp: s.mcp } : {}),
|
|
239
357
|
});
|
|
240
358
|
}
|
|
241
359
|
return out;
|
|
@@ -70,7 +70,11 @@ import { catalogFind } from "./catalog-runtime.js";
|
|
|
70
70
|
import { locateRuntime, type RuntimeFind } from "./runtime-search.js";
|
|
71
71
|
import { claudeProjectsDir } from "../usage/paths.js";
|
|
72
72
|
import { cleanName, transcriptTitle } from "./transcript-follow.js";
|
|
73
|
-
import {
|
|
73
|
+
import {
|
|
74
|
+
claudeConversations,
|
|
75
|
+
transcriptBusy,
|
|
76
|
+
transcriptSummary,
|
|
77
|
+
} from "./conversations.js";
|
|
74
78
|
|
|
75
79
|
/**
|
|
76
80
|
* `/rename "x"` keeps the quotes in customTitle: one matching pair around
|
|
@@ -507,6 +511,14 @@ export const denClaude: Runtime = {
|
|
|
507
511
|
return null;
|
|
508
512
|
}
|
|
509
513
|
},
|
|
514
|
+
// AG-18: mid-turn, from its transcript's tail (never the whole file).
|
|
515
|
+
turnBusy(token, env = process.env) {
|
|
516
|
+
try {
|
|
517
|
+
return transcriptBusy(transcriptFile(token, env));
|
|
518
|
+
} catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
},
|
|
510
522
|
// AG-4b, unofficial (catalog sessionName): `/rename` appends a
|
|
511
523
|
// {"type":"custom-title","customTitle":…} record to the transcript; the
|
|
512
524
|
// latest one, read on from the last ask (never the whole file), less one
|
|
@@ -103,6 +103,25 @@ export interface AgentRecord {
|
|
|
103
103
|
* the list tags it. Start fresh and Relaunch keep the choices.
|
|
104
104
|
*/
|
|
105
105
|
overrides?: Override[];
|
|
106
|
+
/**
|
|
107
|
+
* AG-18: each persona change, oldest first: from, to, when, and who (a
|
|
108
|
+
* person's sign-in, or "cli").
|
|
109
|
+
*/
|
|
110
|
+
personaChanges?: PersonaChange[];
|
|
111
|
+
/**
|
|
112
|
+
* AG-18: a persona change waiting for this agent's turn to end. The wait
|
|
113
|
+
* lives in the server's memory, so a restart drops it; the upkeep at
|
|
114
|
+
* start clears this and audits the cancel (cancelStalePersonaChanges).
|
|
115
|
+
*/
|
|
116
|
+
pendingPersona?: { to: string; since: string; by?: string };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** One persona change of an agent (AG-18). */
|
|
120
|
+
export interface PersonaChange {
|
|
121
|
+
from: string;
|
|
122
|
+
to: string;
|
|
123
|
+
at: string;
|
|
124
|
+
by?: string;
|
|
106
125
|
}
|
|
107
126
|
|
|
108
127
|
/** A setting a session can override for one launch (AG-10.9). */
|
|
@@ -207,6 +207,11 @@ export interface Runtime {
|
|
|
207
207
|
): RuntimeConversation[];
|
|
208
208
|
/** AG-17.1: the folder conversation `token` ran in, or null. Never throws. */
|
|
209
209
|
conversationCwd?(token: string, env?: NodeJS.ProcessEnv): string | null;
|
|
210
|
+
/**
|
|
211
|
+
* AG-18: whether conversation `token` is mid-turn (den-claude: its
|
|
212
|
+
* transcript's tail). None: the runtime can't tell. Never throws.
|
|
213
|
+
*/
|
|
214
|
+
turnBusy?(token: string, env?: NodeJS.ProcessEnv): boolean;
|
|
210
215
|
/**
|
|
211
216
|
* What the runtime prints when it exits at once (0.4.8): `noConversation`
|
|
212
217
|
* when a resume finds nothing to continue, `idInUse` when a new
|
package/src/shared/github-app.ts
CHANGED
|
@@ -83,6 +83,13 @@ export function githubClientId(configured: string | undefined): string {
|
|
|
83
83
|
return (configured ?? MO_DEN_GITHUB_CLIENT_ID).trim();
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The docs section the signed-out page's Learn more opens (SEC-2a-UX):
|
|
88
|
+
* docs/distributions.md, "Signing in with GitHub", in the public repo.
|
|
89
|
+
*/
|
|
90
|
+
export const GITHUB_SIGNIN_DOCS =
|
|
91
|
+
"https://github.com/Monster-Orange/den/blob/main/docs/distributions.md#signing-in-with-github";
|
|
92
|
+
|
|
86
93
|
/** What a GitHub button says while no client id is configured. */
|
|
87
94
|
export function noClientIdMessage(cli: string): string {
|
|
88
95
|
return (
|