@robosystems/core 0.12.0 → 0.13.1

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.
@@ -1,4 +1,6 @@
1
1
  import type { ConsoleConfig } from './types';
2
- export declare function ConsoleContent({ config }: {
2
+ export declare function ConsoleContent({ config, variant, }: {
3
3
  config: ConsoleConfig;
4
+ /** `panel` drops the page header and fills its container (the drawer). */
5
+ variant?: 'page' | 'panel';
4
6
  }): import("react").JSX.Element;
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
6
6
  import { HiTerminal } from 'react-icons/hi';
7
7
  import { useGraphContext } from '../../contexts';
8
8
  import { useStreamingQuery } from '../../hooks';
9
+ import { emitGraphWrites, readGraphWrites, } from '../../lib/graph-writes';
9
10
  import { ConsoleMarkdown } from './ConsoleMarkdown';
10
11
  import { ProgressiveText } from './ProgressiveText';
11
12
  function generateMessageId() {
@@ -53,11 +54,28 @@ function downloadText(filename, text, mime) {
53
54
  anchor.click();
54
55
  URL.revokeObjectURL(url);
55
56
  }
57
+ const WRITE_LABELS = {
58
+ 'create-taxonomy-block': 'Created taxonomy block',
59
+ 'create-information-block': 'Created block',
60
+ 'update-information-block': 'Updated block',
61
+ 'assert-metrics': 'Asserted metrics',
62
+ 'compute-metrics': 'Computed metrics',
63
+ 'compute-forecast': 'Computed forecast',
64
+ 'create-agent': 'Created counterparty',
65
+ 'update-agent': 'Updated counterparty',
66
+ remember: 'Saved memory',
67
+ };
68
+ function describeWrite(write) {
69
+ var _a, _b;
70
+ const label = (_a = WRITE_LABELS[write.operation]) !== null && _a !== void 0 ? _a : write.operation;
71
+ const subject = (_b = write.name) !== null && _b !== void 0 ? _b : write.id;
72
+ return subject ? `${label}: ${subject}` : label;
73
+ }
56
74
  function copyRowsJson(rows) {
57
75
  var _a;
58
76
  (_a = navigator.clipboard) === null || _a === void 0 ? void 0 : _a.writeText(JSON.stringify(rows, null, 2));
59
77
  }
60
- export function ConsoleContent({ config }) {
78
+ export function ConsoleContent({ config, variant = 'page', }) {
61
79
  const { state: graphState } = useGraphContext();
62
80
  const graphId = graphState.currentGraphId;
63
81
  const streamingQuery = useStreamingQuery();
@@ -102,6 +120,9 @@ export function ConsoleContent({ config }) {
102
120
  (config.enableRecall
103
121
  ? ` /recall - Recall semantic memories\n`
104
122
  : '') +
123
+ (config.enableAuthor
124
+ ? ` /do - Make a change (a block, a counterparty, a memory)\n`
125
+ : '') +
105
126
  ` /mcp - Connect an MCP client (sign in — no key)\n` +
106
127
  ` /mcp key - Mint a graph-scoped API key for header-only clients\n` +
107
128
  ` /help - Show this help message\n` +
@@ -145,6 +166,7 @@ export function ConsoleContent({ config }) {
145
166
  setTerminalMessages((prev) => [...prev, message]);
146
167
  }, []);
147
168
  const addResultMessage = useCallback((content, data, cypher, opts) => {
169
+ var _a;
148
170
  const message = {
149
171
  id: generateMessageId(),
150
172
  type: 'system',
@@ -154,6 +176,7 @@ export function ConsoleContent({ config }) {
154
176
  cypher,
155
177
  markdown: opts === null || opts === void 0 ? void 0 : opts.markdown,
156
178
  footer: opts === null || opts === void 0 ? void 0 : opts.footer,
179
+ writes: ((_a = opts === null || opts === void 0 ? void 0 : opts.writes) === null || _a === void 0 ? void 0 : _a.length) ? opts.writes : undefined,
157
180
  };
158
181
  setTerminalMessages((prev) => [...prev, message]);
159
182
  }, []);
@@ -328,7 +351,9 @@ export function ConsoleContent({ config }) {
328
351
  setCurrentQueryStartTime(null);
329
352
  }
330
353
  };
331
- const executeOperatorQuery = async (userQuery) => {
354
+ // `operatorType` names an operator (`author` for /do); unset, the API
355
+ // chooses one for a question.
356
+ const executeOperatorQuery = async (userQuery, operatorType) => {
332
357
  var _a;
333
358
  if (!graphId) {
334
359
  addErrorMessage(config.noSelectionError);
@@ -342,10 +367,11 @@ export function ConsoleContent({ config }) {
342
367
  try {
343
368
  const { clients } = await import('@robosystems/client/clients');
344
369
  const history = conversationRef.current;
345
- const result = await clients.operator.executeQuery(graphId, Object.assign({ message: userQuery,
370
+ const request = Object.assign({ message: userQuery,
346
371
  // `standard` gives the operator loop six tool turns; `quick` (three)
347
372
  // was exhausted by schema + examples before the first query ran.
348
- mode: 'standard' }, (history.length > 0 ? { history } : {})), {
373
+ mode: 'standard' }, (history.length > 0 ? { history } : {}));
374
+ const options = {
349
375
  mode: 'auto',
350
376
  onProgress: (message, percentage) => {
351
377
  if (isStale())
@@ -356,7 +382,10 @@ export function ConsoleContent({ config }) {
356
382
  percentage,
357
383
  });
358
384
  },
359
- });
385
+ };
386
+ const result = operatorType
387
+ ? await clients.operator.executeOperator(graphId, operatorType, request, options)
388
+ : await clients.operator.executeQuery(graphId, request, options);
360
389
  // The user switched graphs while the operator ran: the reset already
361
390
  // cleared the console, so drop the answer rather than print it (and
362
391
  // record it as history) under the new graph.
@@ -378,6 +407,9 @@ export function ConsoleContent({ config }) {
378
407
  }
379
408
  const duration = Date.now() - startTime;
380
409
  const metadata = (result.metadata || {});
410
+ const writes = readGraphWrites(metadata);
411
+ // Pages showing this graph reload what the console just changed.
412
+ emitGraphWrites(requestGraphId, writes);
381
413
  const creditsUsed = metadata.credits_consumed;
382
414
  const resultCount = metadata.result_count;
383
415
  // Prefer the structured outputs the operator loop now returns
@@ -429,7 +461,11 @@ export function ConsoleContent({ config }) {
429
461
  // otherwise collapse under markdown). With no narrative, fall back to the
430
462
  // plain footer, matching the direct-query result style.
431
463
  if (narrative) {
432
- addResultMessage(narrative, rows, cypher, { markdown: true, footer });
464
+ addResultMessage(narrative, rows, cypher, {
465
+ markdown: true,
466
+ footer,
467
+ writes,
468
+ });
433
469
  // Record the exchange for follow-ups. Only a narrative answer is
434
470
  // worth carrying; the answer is clipped so history stays cheap.
435
471
  conversationRef.current = [
@@ -439,7 +475,7 @@ export function ConsoleContent({ config }) {
439
475
  ].slice(-6);
440
476
  }
441
477
  else {
442
- addResultMessage(footer, rows, cypher);
478
+ addResultMessage(footer, rows, cypher, { writes });
443
479
  }
444
480
  }
445
481
  catch (error) {
@@ -596,6 +632,17 @@ export function ConsoleContent({ config }) {
596
632
  }
597
633
  return;
598
634
  }
635
+ // Handle /do — the author operator, which may change the graph. Writes
636
+ // are never inferred from a question; they need this command.
637
+ if (config.enableAuthor && /^\/do(\s|$)/i.test(command)) {
638
+ const request = command.slice(3).trim();
639
+ if (!request) {
640
+ addErrorMessage('Usage: /do <change>\n\nExamples:\n /do add Notion Labs as a vendor\n /do remember that we report ARPC on subscription revenue only');
641
+ return;
642
+ }
643
+ await executeOperatorQuery(request, 'author');
644
+ return;
645
+ }
599
646
  // Handle /recall command — built-in only when the target supports
600
647
  // semantic memory (user graphs; shared repositories reject it). When
601
648
  // disabled it falls through to the unknown-command branch below.
@@ -691,10 +738,11 @@ export function ConsoleContent({ config }) {
691
738
  };
692
739
  // ── Render ──────────────────────────────────────────────────────────
693
740
  const { header } = config;
694
- return (_jsxs("div", { className: "mx-auto max-w-7xl space-y-6 p-4 sm:p-6", children: [_jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-3 sm:gap-4", children: [_jsx("div", { className: `shrink-0 rounded-lg bg-gradient-to-br ${header.gradientFrom} ${header.gradientTo} p-2.5 sm:p-3`, children: _jsx(HiTerminal, { className: "h-6 w-6 text-white sm:h-8 sm:w-8" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "font-heading text-2xl font-bold break-words text-gray-900 sm:text-3xl dark:text-white", children: header.title }), _jsx("p", { className: "mt-1 text-sm break-words text-gray-500 dark:text-gray-400", children: header.subtitle })] })] }), (streamingQuery.isStreaming || operatorProgress.isRunning) && (_jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400", children: [_jsx("span", { className: "inline-block h-2 w-2 animate-pulse rounded-full bg-green-500" }), operatorProgress.isRunning && operatorProgress.message
741
+ const isPanel = variant === 'panel';
742
+ return (_jsxs("div", { className: isPanel ? 'h-full' : 'mx-auto max-w-7xl space-y-6 p-4 sm:p-6', children: [!isPanel && (_jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-3 sm:gap-4", children: [_jsx("div", { className: `shrink-0 rounded-lg bg-gradient-to-br ${header.gradientFrom} ${header.gradientTo} p-2.5 sm:p-3`, children: _jsx(HiTerminal, { className: "h-6 w-6 text-white sm:h-8 sm:w-8" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "font-heading text-2xl font-bold break-words text-gray-900 sm:text-3xl dark:text-white", children: header.title }), _jsx("p", { className: "mt-1 text-sm break-words text-gray-500 dark:text-gray-400", children: header.subtitle })] })] }), (streamingQuery.isStreaming || operatorProgress.isRunning) && (_jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400", children: [_jsx("span", { className: "inline-block h-2 w-2 animate-pulse rounded-full bg-green-500" }), operatorProgress.isRunning && operatorProgress.message
695
743
  ? operatorProgress.message
696
744
  : 'Processing...', operatorProgress.percentage !== undefined &&
697
- ` (${operatorProgress.percentage}%)`] }))] }), _jsx(Card, { className: "overflow-hidden bg-gray-950 !p-0 [&>div]:!p-0", children: _jsxs("div", { className: "flex flex-col bg-gray-950", style: { height: 'calc(100vh - 280px)' }, children: [_jsxs("div", { ref: terminalScrollRef, className: "flex-1 overflow-y-auto p-4 font-mono text-sm", children: [terminalMessages.map((message) => (_jsxs("div", { className: "mb-4", children: [_jsxs("div", { className: "mb-1 flex items-center gap-2 text-xs text-gray-700", children: [_jsx("span", { children: message.timestamp.toLocaleTimeString() }), _jsx("span", { children: "-" }), _jsx("span", { className: "tracking-wider uppercase", children: message.type })] }), _jsxs("div", { className: `leading-relaxed break-words ${message.markdown && !message.isAnimating
745
+ ` (${operatorProgress.percentage}%)`] }))] })), _jsx(Card, { className: `overflow-hidden bg-gray-950 !p-0 [&>div]:!p-0 ${isPanel ? 'h-full rounded-none border-0 [&>div]:h-full' : ''}`, children: _jsxs("div", { className: "flex flex-col bg-gray-950", style: { height: isPanel ? '100%' : 'calc(100vh - 280px)' }, children: [_jsxs("div", { ref: terminalScrollRef, className: "flex-1 overflow-y-auto p-4 font-mono text-sm", children: [terminalMessages.map((message) => (_jsxs("div", { className: "mb-4", children: [_jsxs("div", { className: "mb-1 flex items-center gap-2 text-xs text-gray-700", children: [_jsx("span", { children: message.timestamp.toLocaleTimeString() }), _jsx("span", { children: "-" }), _jsx("span", { className: "tracking-wider uppercase", children: message.type })] }), _jsxs("div", { className: `leading-relaxed break-words ${message.markdown && !message.isAnimating
698
746
  ? ''
699
747
  : 'whitespace-pre-wrap'} ${message.type === 'system'
700
748
  ? 'text-cyan-400'
@@ -702,7 +750,10 @@ export function ConsoleContent({ config }) {
702
750
  ? 'text-green-400'
703
751
  : message.type === 'error'
704
752
  ? 'text-red-400'
705
- : 'text-gray-300'}`, children: [message.type === 'user' && (_jsx("span", { className: "mr-2 text-green-500", children: "$" })), message.isAnimating ? (_jsx(ProgressiveText, { text: message.content, onComplete: () => handleAnimationComplete(message.id), onUpdate: scrollToBottom })) : message.markdown ? (_jsx(ConsoleMarkdown, { content: message.content })) : (message.content)] }), message.footer && (_jsx("div", { className: "mt-3 text-xs whitespace-pre-wrap text-gray-500", children: message.footer })), message.cypher && (_jsxs("div", { className: "mt-3 overflow-hidden rounded border border-gray-800 bg-gray-900/40", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-800 px-3 py-1.5", children: [_jsx("span", { className: "text-xs tracking-wider text-gray-500 uppercase", children: "Generated Cypher" }), _jsx("button", { onClick: () => { var _a; return handleCommand(`/query ${(_a = message.cypher) !== null && _a !== void 0 ? _a : ''}`); }, className: "rounded bg-cyan-600/90 px-2 py-0.5 text-xs font-medium text-white transition-colors hover:bg-cyan-600", children: "Run" })] }), _jsx("pre", { className: "overflow-x-auto px-3 py-2 font-mono text-xs whitespace-pre-wrap text-cyan-300", children: message.cypher })] })), message.data && message.data.length > 0 && (_jsxs("div", { className: "mt-3 overflow-hidden rounded border border-gray-800", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-800 bg-gray-900 px-4 py-2", children: [_jsxs("span", { className: "text-xs text-gray-500", children: [message.data.length, " row", message.data.length === 1 ? '' : 's'] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: () => copyRowsJson(message.data), className: "rounded border border-gray-700 px-2 py-0.5 text-xs text-gray-300 transition-colors hover:bg-gray-800", children: "Copy JSON" }), _jsx("button", { onClick: () => downloadText('results.csv', rowsToCsv(message.data), 'text/csv'), className: "rounded border border-gray-700 px-2 py-0.5 text-xs text-gray-300 transition-colors hover:bg-gray-800", children: "Download CSV" })] })] }), _jsx("div", { className: "max-h-96 overflow-auto", children: _jsxs("table", { className: "w-full border-collapse text-xs", children: [_jsx("thead", { className: "sticky top-0", children: _jsx("tr", { className: "border-b border-gray-800 bg-gray-900", children: Object.keys(message.data[0]).map((key) => (_jsx("th", { className: "px-4 py-2 text-left font-semibold whitespace-nowrap text-cyan-400", children: key }, key))) }) }), _jsx("tbody", { children: message.data
753
+ : 'text-gray-300'}`, children: [message.type === 'user' && (_jsx("span", { className: "mr-2 text-green-500", children: "$" })), message.isAnimating ? (_jsx(ProgressiveText, { text: message.content, onComplete: () => handleAnimationComplete(message.id), onUpdate: scrollToBottom })) : message.markdown ? (_jsx(ConsoleMarkdown, { content: message.content })) : (message.content)] }), message.writes && (_jsxs("div", { className: "mt-3 rounded border border-emerald-900/60 bg-emerald-950/30 px-3 py-2", children: [_jsx("div", { className: "text-xs tracking-wider text-emerald-400 uppercase", children: "Changes made" }), _jsx("ul", { className: "mt-1 space-y-0.5 text-sm text-emerald-200", children: message.writes.map((write, idx) => {
754
+ var _a;
755
+ return (_jsxs("li", { children: ["\u2713 ", describeWrite(write)] }, `${write.operation}-${(_a = write.id) !== null && _a !== void 0 ? _a : idx}`));
756
+ }) })] })), message.footer && (_jsx("div", { className: "mt-3 text-xs whitespace-pre-wrap text-gray-500", children: message.footer })), message.cypher && (_jsxs("div", { className: "mt-3 overflow-hidden rounded border border-gray-800 bg-gray-900/40", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-800 px-3 py-1.5", children: [_jsx("span", { className: "text-xs tracking-wider text-gray-500 uppercase", children: "Generated Cypher" }), _jsx("button", { onClick: () => { var _a; return handleCommand(`/query ${(_a = message.cypher) !== null && _a !== void 0 ? _a : ''}`); }, className: "rounded bg-cyan-600/90 px-2 py-0.5 text-xs font-medium text-white transition-colors hover:bg-cyan-600", children: "Run" })] }), _jsx("pre", { className: "overflow-x-auto px-3 py-2 font-mono text-xs whitespace-pre-wrap text-cyan-300", children: message.cypher })] })), message.data && message.data.length > 0 && (_jsxs("div", { className: "mt-3 overflow-hidden rounded border border-gray-800", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-800 bg-gray-900 px-4 py-2", children: [_jsxs("span", { className: "text-xs text-gray-500", children: [message.data.length, " row", message.data.length === 1 ? '' : 's'] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: () => copyRowsJson(message.data), className: "rounded border border-gray-700 px-2 py-0.5 text-xs text-gray-300 transition-colors hover:bg-gray-800", children: "Copy JSON" }), _jsx("button", { onClick: () => downloadText('results.csv', rowsToCsv(message.data), 'text/csv'), className: "rounded border border-gray-700 px-2 py-0.5 text-xs text-gray-300 transition-colors hover:bg-gray-800", children: "Download CSV" })] })] }), _jsx("div", { className: "max-h-96 overflow-auto", children: _jsxs("table", { className: "w-full border-collapse text-xs", children: [_jsx("thead", { className: "sticky top-0", children: _jsx("tr", { className: "border-b border-gray-800 bg-gray-900", children: Object.keys(message.data[0]).map((key) => (_jsx("th", { className: "px-4 py-2 text-left font-semibold whitespace-nowrap text-cyan-400", children: key }, key))) }) }), _jsx("tbody", { children: message.data
706
757
  .slice(0, MAX_TABLE_ROWS)
707
758
  .map((row, idx) => (_jsx("tr", { className: "border-b border-gray-900 hover:bg-gray-900/50", children: Object.keys(message.data[0]).map((key) => {
708
759
  const value = row[key];
@@ -0,0 +1,15 @@
1
+ import type { ConsoleConfig } from './types';
2
+ /** Height of the closed drawer's bar. */
3
+ export declare const CONSOLE_DRAWER_BAR_HEIGHT = 32;
4
+ /** Set on <html> to the space the drawer covers; pad page content by it. */
5
+ export declare const CONSOLE_DRAWER_HEIGHT_VAR = "--console-drawer-height";
6
+ /**
7
+ * The console as a bottom panel, like VS Code's: a thin bar when closed, a
8
+ * resizable panel when open, toggled with Ctrl+`. It stays mounted once
9
+ * opened, so the conversation survives closing it. `className` positions it,
10
+ * e.g. an offset for the app's sidebar.
11
+ */
12
+ export declare function ConsoleDrawer({ config, className, }: {
13
+ config: ConsoleConfig;
14
+ className?: string;
15
+ }): import("react").JSX.Element;
@@ -0,0 +1,129 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import { HiChevronDown, HiChevronUp, HiOutlineArrowsExpand, HiOutlineMinus, HiTerminal, } from 'react-icons/hi';
5
+ import { ConsoleContent } from './ConsoleContent';
6
+ const STORAGE_KEY = 'robosystems:console-drawer';
7
+ /** Height of the closed drawer's bar. */
8
+ export const CONSOLE_DRAWER_BAR_HEIGHT = 32;
9
+ /** Set on <html> to the space the drawer covers; pad page content by it. */
10
+ export const CONSOLE_DRAWER_HEIGHT_VAR = '--console-drawer-height';
11
+ const DEFAULT_HEIGHT = 320;
12
+ const MIN_HEIGHT = 160;
13
+ // Leave the page's own header in view at full stretch.
14
+ const MAX_HEIGHT_RATIO = 0.8;
15
+ function readState() {
16
+ try {
17
+ const raw = window.localStorage.getItem(STORAGE_KEY);
18
+ if (raw) {
19
+ const parsed = JSON.parse(raw);
20
+ return {
21
+ open: parsed.open === true,
22
+ height: typeof parsed.height === 'number' ? parsed.height : DEFAULT_HEIGHT,
23
+ };
24
+ }
25
+ }
26
+ catch (_a) {
27
+ // Storage blocked or corrupt: start closed at the default height.
28
+ }
29
+ return { open: false, height: DEFAULT_HEIGHT };
30
+ }
31
+ function writeState(state) {
32
+ try {
33
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
34
+ }
35
+ catch (_a) {
36
+ // Not persisting is fine; the drawer still works for this page view.
37
+ }
38
+ }
39
+ function clampHeight(height) {
40
+ const max = Math.floor(window.innerHeight * MAX_HEIGHT_RATIO);
41
+ return Math.min(Math.max(height, MIN_HEIGHT), Math.max(max, MIN_HEIGHT));
42
+ }
43
+ /**
44
+ * The console as a bottom panel, like VS Code's: a thin bar when closed, a
45
+ * resizable panel when open, toggled with Ctrl+`. It stays mounted once
46
+ * opened, so the conversation survives closing it. `className` positions it,
47
+ * e.g. an offset for the app's sidebar.
48
+ */
49
+ export function ConsoleDrawer({ config, className = 'left-0 right-0', }) {
50
+ const [state, setState] = useState({
51
+ open: false,
52
+ height: DEFAULT_HEIGHT,
53
+ });
54
+ // Mount the console on first open, then keep it.
55
+ const [hasOpened, setHasOpened] = useState(false);
56
+ const dragging = useRef(false);
57
+ // The height to return to when a maximized drawer is restored.
58
+ const restoreHeight = useRef(DEFAULT_HEIGHT);
59
+ useEffect(() => {
60
+ const stored = readState();
61
+ setState(Object.assign(Object.assign({}, stored), { height: clampHeight(stored.height) }));
62
+ if (stored.open)
63
+ setHasOpened(true);
64
+ }, []);
65
+ const toggle = useCallback(() => {
66
+ setState((prev) => {
67
+ const merged = Object.assign(Object.assign({}, prev), { open: !prev.open });
68
+ writeState(merged);
69
+ return merged;
70
+ });
71
+ setHasOpened(true);
72
+ }, []);
73
+ useEffect(() => {
74
+ const covered = state.open ? state.height : CONSOLE_DRAWER_BAR_HEIGHT;
75
+ document.documentElement.style.setProperty(CONSOLE_DRAWER_HEIGHT_VAR, `${covered}px`);
76
+ }, [state.open, state.height]);
77
+ useEffect(() => () => {
78
+ document.documentElement.style.removeProperty(CONSOLE_DRAWER_HEIGHT_VAR);
79
+ }, []);
80
+ useEffect(() => {
81
+ const onKey = (event) => {
82
+ if (event.ctrlKey && event.key === '`') {
83
+ event.preventDefault();
84
+ toggle();
85
+ }
86
+ };
87
+ window.addEventListener('keydown', onKey);
88
+ return () => window.removeEventListener('keydown', onKey);
89
+ }, [toggle]);
90
+ const setHeight = (height) => {
91
+ setState((prev) => {
92
+ const merged = Object.assign(Object.assign({}, prev), { height: clampHeight(height) });
93
+ writeState(merged);
94
+ return merged;
95
+ });
96
+ };
97
+ const isMaximized = typeof window !== 'undefined' &&
98
+ state.height >= clampHeight(Number.POSITIVE_INFINITY);
99
+ const toggleMaximized = () => {
100
+ if (isMaximized) {
101
+ setHeight(restoreHeight.current);
102
+ }
103
+ else {
104
+ restoreHeight.current = state.height;
105
+ setHeight(Number.POSITIVE_INFINITY);
106
+ }
107
+ };
108
+ const onResizeStart = (event) => {
109
+ event.preventDefault();
110
+ dragging.current = true;
111
+ const onMove = (move) => {
112
+ if (!dragging.current)
113
+ return;
114
+ setState((prev) => (Object.assign(Object.assign({}, prev), { height: clampHeight(window.innerHeight - move.clientY) })));
115
+ };
116
+ const onUp = () => {
117
+ dragging.current = false;
118
+ window.removeEventListener('pointermove', onMove);
119
+ window.removeEventListener('pointerup', onUp);
120
+ setState((prev) => {
121
+ writeState(prev);
122
+ return prev;
123
+ });
124
+ };
125
+ window.addEventListener('pointermove', onMove);
126
+ window.addEventListener('pointerup', onUp);
127
+ };
128
+ return (_jsxs("div", { className: `fixed bottom-0 z-30 flex flex-col border-t border-gray-800 bg-gray-950 shadow-2xl ${className}`, style: { height: state.open ? state.height : undefined }, "data-testid": "console-drawer", children: [state.open && (_jsx("div", { role: "separator", "aria-orientation": "horizontal", "aria-label": "Resize console", onPointerDown: onResizeStart, onDoubleClick: () => setHeight(DEFAULT_HEIGHT), title: "Drag to resize; double-click to reset", className: "h-1 shrink-0 cursor-row-resize bg-gray-800 hover:bg-cyan-700" })), _jsxs("div", { className: "flex h-8 shrink-0 items-center text-xs text-gray-400", children: [_jsxs("button", { type: "button", onClick: toggle, "aria-expanded": state.open, className: "flex h-full flex-1 items-center gap-2 px-3 hover:text-gray-200", children: [_jsx(HiTerminal, { className: "h-4 w-4" }), _jsx("span", { className: "font-medium tracking-wider uppercase", children: "Console" }), _jsx("span", { className: "text-gray-600", children: "Ctrl+`" }), _jsx("span", { className: "ml-auto", children: state.open ? (_jsx(HiChevronDown, { className: "h-4 w-4" })) : (_jsx(HiChevronUp, { className: "h-4 w-4" })) })] }), state.open && (_jsx("button", { type: "button", onClick: toggleMaximized, "aria-label": isMaximized ? 'Restore console size' : 'Maximize console', title: isMaximized ? 'Restore' : 'Maximize', className: "flex h-full items-center px-3 hover:text-gray-200", children: isMaximized ? (_jsx(HiOutlineMinus, { className: "h-4 w-4" })) : (_jsx(HiOutlineArrowsExpand, { className: "h-4 w-4" })) }))] }), hasOpened && (_jsx("div", { className: `min-h-0 flex-1 ${state.open ? '' : 'hidden'}`, children: _jsx(ConsoleContent, { config: config, variant: "panel" }) }))] }));
129
+ }
@@ -404,6 +404,8 @@ export function buildGraphAwareConsoleConfig(graph, branding) {
404
404
  noSelectionError: (_c = branding.noSelectionError) !== null && _c !== void 0 ? _c : 'No graph selected. Please select a graph first.',
405
405
  // Semantic memory is per-user-graph; shared repositories reject it.
406
406
  enableRecall: !isRepository,
407
+ // Writes need a user graph; the API also checks the write role.
408
+ enableAuthor: !isRepository,
407
409
  };
408
410
  }
409
411
  /**
@@ -1,4 +1,5 @@
1
1
  export { ConsoleContent } from './ConsoleContent';
2
+ export { CONSOLE_DRAWER_BAR_HEIGHT, CONSOLE_DRAWER_HEIGHT_VAR, ConsoleDrawer, } from './ConsoleDrawer';
2
3
  export { ConsoleMarkdown } from './ConsoleMarkdown';
3
4
  export { EXAMPLE_SETS, buildGraphAwareConsoleConfig, getGraphExampleKind, useGraphAwareConsoleConfig, } from './graphAwareConfig';
4
5
  export type { ConsoleBranding, GraphExampleKind, GraphExampleSet, } from './graphAwareConfig';
@@ -1,4 +1,5 @@
1
1
  export { ConsoleContent } from './ConsoleContent';
2
+ export { CONSOLE_DRAWER_BAR_HEIGHT, CONSOLE_DRAWER_HEIGHT_VAR, ConsoleDrawer, } from './ConsoleDrawer';
2
3
  export { ConsoleMarkdown } from './ConsoleMarkdown';
3
4
  export { EXAMPLE_SETS, buildGraphAwareConsoleConfig, getGraphExampleKind, useGraphAwareConsoleConfig, } from './graphAwareConfig';
4
5
  export { ProgressiveText } from './ProgressiveText';
@@ -1,3 +1,4 @@
1
+ import type { GraphWrite } from '../../lib/graph-writes';
1
2
  export interface SampleQuery {
2
3
  name: string;
3
4
  query: string;
@@ -65,6 +66,9 @@ export interface ConsoleConfig {
65
66
  * shared repositories don't support memory, so the graph-aware builder
66
67
  * enables it only for user graphs. */
67
68
  enableRecall?: boolean;
69
+ /** Enable `/do`, which runs the write-capable author operator. The API
70
+ * gates it on the graph write role; off for shared repositories. */
71
+ enableAuthor?: boolean;
68
72
  /** Extra slash commands beyond the built-in set */
69
73
  extraCommands?: ConsoleExtraCommand[];
70
74
  }
@@ -90,4 +94,6 @@ export interface TerminalMessage {
90
94
  * breaks survive markdown rendering.
91
95
  */
92
96
  footer?: string;
97
+ /** Changes the author operator made, shown as a receipt below the answer. */
98
+ writes?: GraphWrite[];
93
99
  }
@@ -1,4 +1,4 @@
1
- export { ConsoleContent } from './console';
1
+ export { CONSOLE_DRAWER_BAR_HEIGHT, CONSOLE_DRAWER_HEIGHT_VAR, ConsoleContent, ConsoleDrawer, } from './console';
2
2
  export { EntitySelector, type EntityGroup, type SelectableEntity, } from './EntitySelector';
3
3
  export { EntitySelectorCore, type EntityLike, type EntityRecord, type EntitySelectorProps, type GraphWithEntities, } from './EntitySelectorCore';
4
4
  export { GraphFilters, byGraphType, composeFilters, excludeRepositories, excludeSubgraphs, hasAllSchemaExtensions, hasAnySchemaExtension, hasSchemaExtension, onlyEntityGraphs, onlyGenericGraphs, onlyRepositories, onlyUserGraphs, } from './graph-filters';
@@ -1,4 +1,4 @@
1
- export { ConsoleContent } from './console';
1
+ export { CONSOLE_DRAWER_BAR_HEIGHT, CONSOLE_DRAWER_HEIGHT_VAR, ConsoleContent, ConsoleDrawer, } from './console';
2
2
  export { EntitySelector, } from './EntitySelector';
3
3
  export { EntitySelectorCore, } from './EntitySelectorCore';
4
4
  export { GraphFilters, byGraphType, composeFilters, excludeRepositories, excludeSubgraphs, hasAllSchemaExtensions, hasAnySchemaExtension, hasSchemaExtension, onlyEntityGraphs, onlyGenericGraphs, onlyRepositories, onlyUserGraphs, } from './graph-filters';
package/hooks/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { useApiError } from './use-api-error';
2
2
  export { useCrossAppLink, type CrossAppTarget, type OpenCrossAppOptions, type UseCrossAppLinkResult, } from './use-cross-app-link';
3
+ export { useGraphWrites } from './use-graph-writes';
3
4
  export { useMediaQuery } from './use-media-query';
4
5
  export { useStreamingQuery } from './use-streaming-query';
5
6
  export { useToast, type ToastMessage } from './use-toast';
package/hooks/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { useApiError } from './use-api-error';
2
2
  export { useCrossAppLink, } from './use-cross-app-link';
3
+ export { useGraphWrites } from './use-graph-writes';
3
4
  export { useMediaQuery } from './use-media-query';
4
5
  export { useStreamingQuery } from './use-streaming-query';
5
6
  export { useToast } from './use-toast';
@@ -0,0 +1,6 @@
1
+ import { type GraphWrite } from '../lib/graph-writes';
2
+ /**
3
+ * Calls `onWrites` when the console changes the graph a page is showing, so
4
+ * the page can reload what it displays. Writes to other graphs are ignored.
5
+ */
6
+ export declare function useGraphWrites(graphId: string | null | undefined, onWrites: (writes: GraphWrite[]) => void): void;
@@ -0,0 +1,22 @@
1
+ 'use client';
2
+ import { useEffect, useRef } from 'react';
3
+ import { GRAPH_WRITES_EVENT, } from '../lib/graph-writes';
4
+ /**
5
+ * Calls `onWrites` when the console changes the graph a page is showing, so
6
+ * the page can reload what it displays. Writes to other graphs are ignored.
7
+ */
8
+ export function useGraphWrites(graphId, onWrites) {
9
+ const onWritesRef = useRef(onWrites);
10
+ onWritesRef.current = onWrites;
11
+ useEffect(() => {
12
+ if (!graphId)
13
+ return;
14
+ const handler = (event) => {
15
+ const detail = event.detail;
16
+ if ((detail === null || detail === void 0 ? void 0 : detail.graphId) === graphId)
17
+ onWritesRef.current(detail.writes);
18
+ };
19
+ window.addEventListener(GRAPH_WRITES_EVENT, handler);
20
+ return () => window.removeEventListener(GRAPH_WRITES_EVENT, handler);
21
+ }, [graphId]);
22
+ }
package/index.d.ts CHANGED
@@ -2,16 +2,16 @@ export * as AuthCore from './auth-core';
2
2
  export { clearToken, getAuthHeader, getRefreshToken, getToken, getValidToken, handleAuthResponse, hasValidToken, storeToken, } from './auth-core/token-storage';
3
3
  export { clearUserCookies, clearUserLocalStorage, clearUserSessionStorage, listUserData, performLogoutCleanup, resetUIState, } from './auth-core/cleanup';
4
4
  export { AppSwitcher, AuthGuard, AuthProvider, LoginRedirector, SignInForm, SignUpForm, useAuth, } from './auth-components';
5
- export { useApiError, useMediaQuery, useToast, useUser, useUserLimits, type ToastMessage, } from './hooks';
5
+ export { useApiError, useGraphWrites, useMediaQuery, useToast, useUser, useUserLimits, type ToastMessage, } from './hooks';
6
6
  export { createGraphContext, createGraphProvider, createUseGraphContext, EntityProvider, GraphContext, GraphProvider, OrgProvider, ServiceOfferingsProvider, SidebarProvider, useEntity, useGraphContext, useOrg, useServiceOfferings, useSidebarContext, type EntityContextValue, type EntityProviderProps, type GraphContextValue, type GraphProviderProps, type GraphState, type OrgContextState, type OrgContextValue, type OrgProviderProps, type ServiceOfferings, } from './contexts';
7
- export { clientGraphCookie, clientSidebarCookie, createMcpConnectorUrl, entityCookie, graphCookie, sidebarCookie, type EntityCookie, type GraphCookie, type McpConnectorUrl, type SidebarCookie, } from './lib';
7
+ export { clientGraphCookie, clientSidebarCookie, createMcpConnectorUrl, emitGraphWrites, entityCookie, GRAPH_WRITES_EVENT, graphCookie, readGraphWrites, sidebarCookie, type EntityCookie, type GraphCookie, type GraphWrite, type GraphWritesDetail, type McpConnectorUrl, type SidebarCookie, } from './lib';
8
8
  export { ApiError, errorStatus, extractErrorDetail, isApiError, isSessionRejection, isTransientError, toApiError, unwrapSdk, type SdkResult, } from './lib/sdk-errors';
9
9
  export { generateEntityUri, isUUID, UUID_REGEX } from './utils';
10
10
  export { clearGraphSelection, getGraphSelection, persistGraphSelection, } from './actions/graph-actions';
11
11
  export { clearEntitySelection, getEntitySelection, persistEntitySelection, } from './actions/entity-actions';
12
12
  export type { Entities, Entity, User } from './types';
13
13
  export { customTheme } from './theme';
14
- export { buildGraphAwareConsoleConfig, ConsoleContent, EXAMPLE_SETS, getGraphExampleKind, useGraphAwareConsoleConfig, } from './components/console';
14
+ export { buildGraphAwareConsoleConfig, CONSOLE_DRAWER_BAR_HEIGHT, CONSOLE_DRAWER_HEIGHT_VAR, ConsoleContent, ConsoleDrawer, EXAMPLE_SETS, getGraphExampleKind, useGraphAwareConsoleConfig, } from './components/console';
15
15
  export type { ConsoleBranding, ConsoleCommandContext, ConsoleConfig, ConsoleExtraCommand, ConsoleHeaderConfig, ConsoleMcpConfig, ConsoleWelcomeConfig, GraphExampleKind, GraphExampleSet, SampleQuery, } from './components/console';
16
16
  export { anchoredMessage, errorDetail, ReportChat, type ReportChatProps, } from './components/report-chat';
17
17
  export { ActiveSubscriptions, BrowseRepositories, EntitySelector, EntitySelectorCore, GraphSelectorCore, PageLayout, RepositoryGuard, SearchBar, SearchContent, SearchHitCard, SearchPagination, SearchResultsMeta, useIsRepository, type ActiveSubscriptionsProps, type BrowseRepositoriesProps, type EntityGroup, type EntityLike, type EntityRecord, type EntitySelectorProps, type GraphSelectorProps, type GraphWithEntities, type SearchBarProps, type SearchConfig, type SearchFilterConfig, type SearchHitCardProps, type SearchPaginationProps, type SearchResultsMetaProps, type SelectableEntity, } from './components';
package/index.js CHANGED
@@ -8,7 +8,7 @@ export { clearUserCookies, clearUserLocalStorage, clearUserSessionStorage, listU
8
8
  // Export auth-components
9
9
  export { AppSwitcher, AuthGuard, AuthProvider, LoginRedirector, SignInForm, SignUpForm, useAuth, } from './auth-components';
10
10
  // Export hooks
11
- export { useApiError, useMediaQuery, useToast, useUser, useUserLimits, } from './hooks';
11
+ export { useApiError, useGraphWrites, useMediaQuery, useToast, useUser, useUserLimits, } from './hooks';
12
12
  // Export contexts
13
13
  export {
14
14
  // Graph context exports
@@ -20,7 +20,7 @@ OrgProvider,
20
20
  // Service offerings context
21
21
  ServiceOfferingsProvider, SidebarProvider, useEntity, useGraphContext, useOrg, useServiceOfferings, useSidebarContext, } from './contexts';
22
22
  // Export lib utilities
23
- export { clientGraphCookie, clientSidebarCookie, createMcpConnectorUrl, entityCookie, graphCookie, sidebarCookie, } from './lib';
23
+ export { clientGraphCookie, clientSidebarCookie, createMcpConnectorUrl, emitGraphWrites, entityCookie, GRAPH_WRITES_EVENT, graphCookie, readGraphWrites, sidebarCookie, } from './lib';
24
24
  // Export the SDK error seam (unwrap an SDK result or throw ApiError)
25
25
  export { ApiError, errorStatus, extractErrorDetail, isApiError, isSessionRejection, isTransientError, toApiError, unwrapSdk, } from './lib/sdk-errors';
26
26
  // Export utils
@@ -32,7 +32,7 @@ export { clearEntitySelection, getEntitySelection, persistEntitySelection, } fro
32
32
  export { customTheme } from './theme';
33
33
  // Export components
34
34
  // Export console components
35
- export { buildGraphAwareConsoleConfig, ConsoleContent, EXAMPLE_SETS, getGraphExampleKind, useGraphAwareConsoleConfig, } from './components/console';
35
+ export { buildGraphAwareConsoleConfig, CONSOLE_DRAWER_BAR_HEIGHT, CONSOLE_DRAWER_HEIGHT_VAR, ConsoleContent, ConsoleDrawer, EXAMPLE_SETS, getGraphExampleKind, useGraphAwareConsoleConfig, } from './components/console';
36
36
  export { anchoredMessage, errorDetail, ReportChat, } from './components/report-chat';
37
37
  export { ActiveSubscriptions, BrowseRepositories, EntitySelector, EntitySelectorCore, GraphSelectorCore, PageLayout, RepositoryGuard, SearchBar, SearchContent, SearchHitCard, SearchPagination, SearchResultsMeta, useIsRepository, } from './components';
38
38
  // Export graph filters
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A write an operator made, as the API reports it in `metadata.writes`.
3
+ * `id` is null when the tool returned no object id (for example a memory).
4
+ */
5
+ export interface GraphWrite {
6
+ operation: string;
7
+ id: string | null;
8
+ name: string | null;
9
+ block_type?: string;
10
+ }
11
+ export interface GraphWritesDetail {
12
+ graphId: string;
13
+ writes: GraphWrite[];
14
+ }
15
+ /** Window event fired after the console changes a graph. */
16
+ export declare const GRAPH_WRITES_EVENT = "robosystems:graph-writes";
17
+ /** Tell any page showing `graphId` that it changed. No-op when nothing was written. */
18
+ export declare function emitGraphWrites(graphId: string, writes: GraphWrite[]): void;
19
+ /** Read `metadata.writes` defensively: anything malformed yields no writes. */
20
+ export declare function readGraphWrites(metadata: unknown): GraphWrite[];
@@ -0,0 +1,19 @@
1
+ /** Window event fired after the console changes a graph. */
2
+ export const GRAPH_WRITES_EVENT = 'robosystems:graph-writes';
3
+ /** Tell any page showing `graphId` that it changed. No-op when nothing was written. */
4
+ export function emitGraphWrites(graphId, writes) {
5
+ if (typeof window === 'undefined' || writes.length === 0)
6
+ return;
7
+ window.dispatchEvent(new CustomEvent(GRAPH_WRITES_EVENT, {
8
+ detail: { graphId, writes },
9
+ }));
10
+ }
11
+ /** Read `metadata.writes` defensively: anything malformed yields no writes. */
12
+ export function readGraphWrites(metadata) {
13
+ const writes = metadata === null || metadata === void 0 ? void 0 : metadata.writes;
14
+ if (!Array.isArray(writes))
15
+ return [];
16
+ return writes.filter((w) => typeof w === 'object' &&
17
+ w !== null &&
18
+ typeof w.operation === 'string');
19
+ }
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { clientSidebarCookie, sidebarCookie, type SidebarCookie, } from './sidebar-cookie';
2
2
  export { clientGraphCookie, graphCookie, type GraphCookie, } from './graph-cookie';
3
3
  export { entityCookie, type EntityCookie } from './entity-cookie';
4
+ export { GRAPH_WRITES_EVENT, emitGraphWrites, readGraphWrites, type GraphWrite, type GraphWritesDetail, } from './graph-writes';
4
5
  export { createMcpConnectorUrl, type McpConnectorUrl } from './mcp-connector';
5
6
  export { ApiError, errorStatus, extractErrorDetail, isApiError, isSessionRejection, isTransientError, toApiError, unwrapSdk, type SdkResult, } from './sdk-errors';
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { clientSidebarCookie, sidebarCookie, } from './sidebar-cookie';
2
2
  export { clientGraphCookie, graphCookie, } from './graph-cookie';
3
3
  export { entityCookie } from './entity-cookie';
4
+ export { GRAPH_WRITES_EVENT, emitGraphWrites, readGraphWrites, } from './graph-writes';
4
5
  export { createMcpConnectorUrl } from './mcp-connector';
5
6
  export { ApiError, errorStatus, extractErrorDetail, isApiError, isSessionRejection, isTransientError, toApiError, unwrapSdk, } from './sdk-errors';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robosystems/core",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "Shared RoboSystems frontend core library (auth, contexts, task monitoring, UI components) for the RoboSystems Next.js apps",
5
5
  "license": "MIT",
6
6
  "private": false,