@stratusagent/dashboard 0.11.2 → 0.11.3
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/README.md +1 -1
- package/package.json +1 -1
- package/ui/app.js +6 -1
- package/ui/lib/grants.d.ts +6 -0
- package/ui/lib/grants.js +31 -0
- package/ui/views/agent.js +7 -2
- package/ui/views/dashboard.js +10 -1
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ conversations, and a live activity feed.
|
|
|
52
52
|
**Agent** — one agent, three tabs. *Chat* streams a conversation as it
|
|
53
53
|
happens: text arrives token by token, tool calls appear as status lines, and
|
|
54
54
|
the transcript is re-read from the daemon when the turn settles so the page
|
|
55
|
-
never quietly disagrees with what is stored. *Activity* is the same event
|
|
55
|
+
never quietly disagrees with what is stored. It is re-read the same way when a message an agent overheard lands in the conversation with no turn at all — `session.observed` is the only sign of it, and the activity feed shows it as *overheard a message*. *Activity* is the same event
|
|
56
56
|
stream filtered to that agent. *Settings* edits the soul — name, persona,
|
|
57
57
|
provider and model pins — through a validated round-trip.
|
|
58
58
|
|
package/package.json
CHANGED
package/ui/app.js
CHANGED
|
@@ -162,6 +162,9 @@ const describe = (envelope) => {
|
|
|
162
162
|
case 'session.created': return 'started a conversation';
|
|
163
163
|
case 'session.completed': return 'finished a turn';
|
|
164
164
|
case 'session.failed': return `turn failed — ${event.error}`;
|
|
165
|
+
// News, unlike `session.updated`: a message entered the conversation
|
|
166
|
+
// with no turn to announce it, so this line is the only sign of it.
|
|
167
|
+
case 'session.observed': return 'overheard a message';
|
|
165
168
|
case 'tool.called': return `ran ${event.call.toolName}`;
|
|
166
169
|
case 'tool.completed': return `${event.result.toolName} ${event.result.ok ? 'succeeded' : 'failed'}`;
|
|
167
170
|
case 'tool.denied': return `${event.call.toolName} was denied`;
|
|
@@ -220,7 +223,9 @@ const handleEnvelope = (envelope) => {
|
|
|
220
223
|
if (type === 'tool.approval-requested' || type === 'tool.approval-resolved') {
|
|
221
224
|
void refreshCore();
|
|
222
225
|
}
|
|
223
|
-
|
|
226
|
+
// An overhear belongs here too: it moves the session's own timestamp,
|
|
227
|
+
// which is what the overview orders by, and emits nothing else.
|
|
228
|
+
if (type === 'session.completed' || type === 'session.failed' || type === 'session.created' || type === 'session.observed') {
|
|
224
229
|
void refreshCore();
|
|
225
230
|
void refreshForRoute();
|
|
226
231
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Types for the one UI module a test imports. The UI itself is plain ES
|
|
2
|
+
// modules with no build step; this declaration is what lets `pnpm typecheck`
|
|
3
|
+
// cover the test without pretending the module is TypeScript.
|
|
4
|
+
export type AlwaysMeans = 'scope' | 'origin' | 'tool' | 'session';
|
|
5
|
+
/** The line shown beside **Always allow**, or undefined when the lifetime is unknown. */
|
|
6
|
+
export const alwaysOffer: (always: AlwaysMeans | undefined, agentName: string) => string | undefined;
|
package/ui/lib/grants.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What **Always allow** would actually do, said beside the button.
|
|
3
|
+
*
|
|
4
|
+
* The lifetime is the half of the question that button widens, and it
|
|
5
|
+
* differs by tool: a standing grant to the agent outlives every session and
|
|
6
|
+
* every restart, while a send outside a schedule lasts only this
|
|
7
|
+
* conversation. An operator cannot tell those apart from a button reading
|
|
8
|
+
* "Always allow" — which is the whole defect step 28 exists to close, so
|
|
9
|
+
* leaving this surface silent would reintroduce it here.
|
|
10
|
+
*
|
|
11
|
+
* `always` rides in on the request (`GET /approvals` and
|
|
12
|
+
* `tool.approval-requested`), so this never guesses: it is absent exactly
|
|
13
|
+
* when the answer is remembered nowhere, and `oneShot` already covers that
|
|
14
|
+
* case with its own line.
|
|
15
|
+
*/
|
|
16
|
+
export const alwaysOffer = (always, agentName) => {
|
|
17
|
+
switch (always) {
|
|
18
|
+
case 'tool':
|
|
19
|
+
return `Always allow grants this tool to ${agentName} until an operator revokes it.`;
|
|
20
|
+
case 'session':
|
|
21
|
+
return 'Always allow stops this tool asking again for the rest of this session.';
|
|
22
|
+
case 'scope':
|
|
23
|
+
return "Always allow remembers this command's scope for this agent, until revoked.";
|
|
24
|
+
case 'origin':
|
|
25
|
+
return 'Always allow remembers this site for this agent, until revoked.';
|
|
26
|
+
default:
|
|
27
|
+
// A request from a daemon that does not label its approvals — an older
|
|
28
|
+
// one across an upgrade. Saying nothing beats guessing at a lifetime.
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
};
|
package/ui/views/agent.js
CHANGED
|
@@ -383,9 +383,14 @@ export const renderAgent = (agentId, initialSessionId) => {
|
|
|
383
383
|
|
|
384
384
|
// Anything that changes the stored conversation is worth re-reading even
|
|
385
385
|
// when somebody else caused it — it is the same conversation. Only the
|
|
386
|
-
// in-flight rendering below is ours alone.
|
|
386
|
+
// in-flight rendering below is ours alone. An overhear is always
|
|
387
|
+
// somebody else's: it runs no turn, so it carries no turn id and this
|
|
388
|
+
// event is the only one it emits — and a message in the transcript
|
|
389
|
+
// that no completion follows is exactly the change this branch exists
|
|
390
|
+
// to notice. The session list re-reads too, since its order follows
|
|
391
|
+
// the row's own timestamp, which an overhear moves.
|
|
387
392
|
if (!isOurTurn(envelope)) {
|
|
388
|
-
if (event.type === 'session.completed' || event.type === 'session.failed') {
|
|
393
|
+
if (event.type === 'session.completed' || event.type === 'session.failed' || event.type === 'session.observed') {
|
|
389
394
|
void loadTranscript();
|
|
390
395
|
void loadSessions();
|
|
391
396
|
}
|
package/ui/views/dashboard.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ago, duration, el, until } from '../lib/dom.js';
|
|
2
2
|
import { api } from '../lib/api.js';
|
|
3
3
|
import { avatar } from '../lib/avatar.js';
|
|
4
|
+
import { alwaysOffer } from '../lib/grants.js';
|
|
4
5
|
import { isActive, navigate, refreshCore, store } from '../app.js';
|
|
5
6
|
|
|
6
7
|
export const activityPanel = (filter) => {
|
|
@@ -46,6 +47,7 @@ const approvalCard = () => {
|
|
|
46
47
|
),
|
|
47
48
|
el('div', { class: 'approvals-list' }, ...store.approvals.map((request) => {
|
|
48
49
|
const agent = store.agents.find((candidate) => candidate.id === request.agentId);
|
|
50
|
+
const offer = request.oneShot ? undefined : alwaysOffer(request.always, agent?.name ?? request.agentId);
|
|
49
51
|
return el('div', { class: 'approval' },
|
|
50
52
|
el('div', { class: 'row' },
|
|
51
53
|
agent ? avatar(agent, 24) : null,
|
|
@@ -66,9 +68,16 @@ const approvalCard = () => {
|
|
|
66
68
|
// answer — a `dangerous` tool, or a browser action with no page to
|
|
67
69
|
// grant. There it does exactly what **Allow once** does, under a
|
|
68
70
|
// label promising a standing grant nobody gets.
|
|
71
|
+
//
|
|
72
|
+
// And where there IS one, what it grants: for most gated tools that
|
|
73
|
+
// is a standing grant to the agent, outliving this session and every
|
|
74
|
+
// restart, which is a larger thing to hand out than the session the
|
|
75
|
+
// button used to buy.
|
|
69
76
|
request.oneShot
|
|
70
77
|
? el('div', { class: 'sub' }, 'An approval covers this call only — nothing about it is remembered.')
|
|
71
|
-
:
|
|
78
|
+
: offer
|
|
79
|
+
? el('div', { class: 'sub' }, offer)
|
|
80
|
+
: null,
|
|
72
81
|
el('div', { class: 'actions' },
|
|
73
82
|
el('button', { class: 'primary small', onClick: () => decide(request.requestId, 'once') }, 'Allow once'),
|
|
74
83
|
request.oneShot
|