@stratusagent/dashboard 0.11.1 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stratusagent/dashboard",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
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
- if (type === 'session.completed' || type === 'session.failed' || type === 'session.created') {
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;
@@ -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
  }
@@ -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
- : null,
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