@dotdrelle/wiki-manager 0.15.48 → 0.15.50

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.
@@ -728,6 +728,57 @@ test('runtime server kill can target a specific run id', async (t) => {
728
728
  }
729
729
  });
730
730
 
731
+ test('runtime server kill purge without workspace is refused', async (t) => {
732
+ let clearedArgs = null;
733
+ let handle;
734
+ try {
735
+ handle = await startRuntimeServer({
736
+ host: '127.0.0.1',
737
+ port: 0,
738
+ store: {
739
+ dbPath: ':memory:',
740
+ getState: () => ({ status: 'idle' }),
741
+ listEvents: () => [],
742
+ interruptRuns: () => 0,
743
+ cancelActiveTasksForInterruptedRuns: () => 0,
744
+ clearWorkspaceState: (args) => {
745
+ clearedArgs = args;
746
+ return { runs: 0, events: 0, queue: 0 };
747
+ },
748
+ },
749
+ session: {},
750
+ run: async () => {},
751
+ });
752
+ } catch (err) {
753
+ if (err?.code === 'EPERM') {
754
+ t.skip('network listen is not permitted in this sandbox');
755
+ return;
756
+ }
757
+ throw err;
758
+ }
759
+
760
+ try {
761
+ const unscoped = await fetch(`http://127.0.0.1:${handle.port}/kill`, {
762
+ method: 'POST',
763
+ headers: { 'Content-Type': 'application/json' },
764
+ body: JSON.stringify({ purge: true }),
765
+ });
766
+ assert.equal(unscoped.status, 400);
767
+ assert.deepEqual(await unscoped.json(), { killed: false, reason: 'workspace_required' });
768
+ assert.equal(clearedArgs, null);
769
+
770
+ const scoped = await fetch(`http://127.0.0.1:${handle.port}/kill?workspace=docs`, {
771
+ method: 'POST',
772
+ headers: { 'Content-Type': 'application/json' },
773
+ body: JSON.stringify({ purge: true }),
774
+ });
775
+ assert.equal(scoped.status, 202);
776
+ assert.equal(clearedArgs.workspace, 'docs');
777
+ } finally {
778
+ await handle.close();
779
+ }
780
+ });
781
+
731
782
  test('runtime server state exposes active run identity while running', async (t) => {
732
783
  let releaseRun;
733
784
  const context = {
@@ -1031,6 +1082,7 @@ test('runtime server exposes approval endpoint', async (t) => {
1031
1082
  groupId: null,
1032
1083
  planRevision: null,
1033
1084
  approvalClasses: [],
1085
+ caller: null,
1034
1086
  });
1035
1087
  assert.deepEqual(await response.json(), { approved: true, runId: 'run-1', itemId: 'item-1' });
1036
1088
  } finally {
@@ -1256,11 +1308,21 @@ test('runtime server control message handles approve and cancel intents during a
1256
1308
  const approveResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
1257
1309
  method: 'POST',
1258
1310
  headers: { 'Content-Type': 'application/json' },
1259
- body: JSON.stringify({ action: 'message', input: 'valide tout' }),
1311
+ body: JSON.stringify({ action: 'message', input: 'valide tout', intent: 'approve' }),
1260
1312
  });
1261
1313
  assert.equal(approveResponse.status, 200);
1262
1314
  assert.equal((await approveResponse.json()).kind, 'approve');
1263
1315
 
1316
+ // Free-text approval phrasing is NOT honored anymore: approval is an
1317
+ // explicit /approve (or the approval button), never a keyword match.
1318
+ const freeTextResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
1319
+ method: 'POST',
1320
+ headers: { 'Content-Type': 'application/json' },
1321
+ body: JSON.stringify({ action: 'message', input: 'valide tout' }),
1322
+ });
1323
+ assert.equal(freeTextResponse.status, 200);
1324
+ assert.notEqual((await freeTextResponse.json()).kind, 'approve');
1325
+
1264
1326
  const cancelResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
1265
1327
  method: 'POST',
1266
1328
  headers: { 'Content-Type': 'application/json' },
@@ -13,6 +13,7 @@ export function startActivitySupervisor(session, {
13
13
  agentRegistryIntervalMs = registryIntervalFromEnv(),
14
14
  agentRegistry = null,
15
15
  callTool = callMcpTool,
16
+ refreshMcp = null,
16
17
  } = {}) {
17
18
  const pollBusy = new Set();
18
19
  let stopped = false;
@@ -32,8 +33,24 @@ export function startActivitySupervisor(session, {
32
33
  }
33
34
  }, queueIntervalMs);
34
35
 
36
+ /*
37
+ The re-scan must probe the endpoints again, not trust a stale status.
38
+
39
+ The first discovery ran while the containers were still coming up, so the
40
+ production endpoint was marked "not connected" and its agent fell back to a
41
+ legacy placeholder. Re-scanning against that cached status kept the fallback
42
+ forever. `refreshMcp` re-resolves the endpoint states (and their tools)
43
+ before each discovery, so a container that came up is actually discovered.
44
+ */
35
45
  const agentRegistryTimer = agentRegistryIntervalMs > 0
36
- ? setInterval(() => {
46
+ ? setInterval(async () => {
47
+ if (refreshMcp) {
48
+ try {
49
+ await refreshMcp(session);
50
+ } catch {
51
+ /* the discovery below still runs on the cached status */
52
+ }
53
+ }
37
54
  void discoverAgentsOnce(session, { registry, signal: runSignal });
38
55
  }, agentRegistryIntervalMs)
39
56
  : null;
@@ -246,6 +246,49 @@ test('startActivitySupervisor periodically re-scans the agent registry', async (
246
246
  }
247
247
  });
248
248
 
249
+ test('startActivitySupervisor refreshes MCP before each periodic re-scan', async () => {
250
+ /*
251
+ Without the refresh, the re-scan re-reads a cached "not connected" endpoint
252
+ status and keeps a legacy placeholder forever, so an agent whose container
253
+ came up after boot is never discovered. The refresh probes the endpoints
254
+ again before the scan.
255
+ */
256
+ const session = {
257
+ mcp: {},
258
+ activities: {},
259
+ headlessPlan: null,
260
+ jobQueue: [],
261
+ };
262
+ let refreshes = 0;
263
+ let discoveries = 0;
264
+ const registry = {
265
+ async discover() {
266
+ discoveries += 1;
267
+ return [];
268
+ },
269
+ snapshot() {
270
+ return [];
271
+ },
272
+ };
273
+
274
+ const supervisor = startActivitySupervisor(session, {
275
+ intervalMs: 1000,
276
+ queueIntervalMs: 1000,
277
+ agentRegistryIntervalMs: 10,
278
+ agentRegistry: registry,
279
+ refreshMcp: async () => {
280
+ refreshes += 1;
281
+ },
282
+ });
283
+
284
+ try {
285
+ await waitFor(() => refreshes >= 2);
286
+ assert.ok(discoveries >= 2);
287
+ } finally {
288
+ supervisor.stop();
289
+ }
290
+ });
291
+
249
292
  test('discoverAgentsOnce uses the session registry and returns discovered agents', async () => {
250
293
  const expected = [{ agentInstanceId: 'a' }];
251
294
  const registry = {
@@ -822,14 +822,17 @@ export function LeftPane(props: {
822
822
  const showWelcome = () => props.showWelcome && props.messages.length === 0;
823
823
  return (
824
824
  <box width={props.width} height="100%" flexDirection="column" padding={1} overflow="hidden">
825
- <box height={3} flexDirection="column">
825
+ {/*
826
+ The header keeps what changes the MEANING of what is typed — the mode,
827
+ and the transient hint. Workspace, version and connection states moved
828
+ to the foot of the pane: they are an ambient state one glances at, not
829
+ something read before every message, and at the top they pushed the
830
+ conversation down by a line for standing information.
831
+ */}
832
+ <box height={2} flexDirection="column">
826
833
  <box height={1} flexDirection="row" backgroundColor={modeColor()} paddingX={1}>
827
834
  <text fg="#0B1020">{modeLabel()}</text>
828
835
  </box>
829
- <box height={1} flexDirection="row">
830
- <text fg="#D6DEE8">{props.title}</text>
831
- <text fg="#7F8C8D"> {props.statusLine}</text>
832
- </box>
833
836
  <box height={1} flexDirection="row">
834
837
  {props.hintLine ? <text fg="#FBBF24">[ {props.hintLine} ]</text> : null}
835
838
  </box>
@@ -860,6 +863,15 @@ export function LeftPane(props: {
860
863
  onSubmit={props.submit}
861
864
  onHeightChange={props.onInputHeightChange}
862
865
  />
866
+ {/*
867
+ Status foot. One line, as in the header before it, so the row budget of
868
+ `conversationRows` (height - 5 - input) is unchanged: the line moved,
869
+ it was not added.
870
+ */}
871
+ <box height={1} flexDirection="row">
872
+ <text fg="#D6DEE8">{props.title}</text>
873
+ <text fg="#7F8C8D"> {props.statusLine}</text>
874
+ </box>
863
875
  </box>
864
876
  );
865
877
  }
@@ -249,7 +249,15 @@ export function StartupScreen(props: {
249
249
  return `${workspace} / ${profile}`;
250
250
  });
251
251
 
252
- const innerWidth = createMemo(() => Math.max(40, panelWidth() - 6));
252
+ /*
253
+ Usable width inside the panel.
254
+
255
+ The panel has no border any more, only `padding={2}`: 4 columns, not 6. The
256
+ two extra columns were the border's, and keeping them would leave the text
257
+ measured for a frame that is no longer drawn — every `fit()` truncating two
258
+ characters early for nothing.
259
+ */
260
+ const innerWidth = createMemo(() => Math.max(40, panelWidth() - 4));
253
261
  const shortcutHint = createMemo(() => {
254
262
  const count = currentLength();
255
263
  const quick = count === 1 ? '1 quick select' : `1-${count} quick select`;
@@ -267,9 +275,6 @@ export function StartupScreen(props: {
267
275
  top={top()}
268
276
  width={panelWidth()}
269
277
  height={panelHeight()}
270
- border
271
- borderStyle="rounded"
272
- borderColor="#8BD5CA"
273
278
  backgroundColor="#111318"
274
279
  padding={2}
275
280
  flexDirection="column"
package/src/shell/repl.js CHANGED
@@ -2115,6 +2115,7 @@ async function runTuiShell({ agent, packageJson, session, runtime = null }) {
2115
2115
  url: runtime.url,
2116
2116
  workspace: session.workspace ?? null,
2117
2117
  ...(kind === 'item' ? { itemId: id } : kind === 'approval' ? { approvalId: id } : { runId: id }),
2118
+ caller: 'shell-/approve',
2118
2119
  });
2119
2120
  conversationMessages(session).push({ role: 'command', content: `Approval ${result.approved ? 'accepted' : 'not found'}: ${id}` });
2120
2121
  syncRuntimeState();