@vention/vention-skills 0.10.15 → 0.10.16

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,6 +1,6 @@
1
1
  {
2
2
  "name": "vention",
3
- "version": "0.10.15",
3
+ "version": "0.10.16",
4
4
  "description": "Vention skills for coding agents: write MachineLogic applications, build machine designs, and diagnose deployed machines.",
5
5
  "author": {
6
6
  "name": "Vention"
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vention skills
2
2
 
3
- Version 0.10.15. Skills for coding agents that work with Vention machines, packaged as one plugin, `vention`, in the `vention-skills` marketplace.
3
+ Version 0.10.16. Skills for coding agents that work with Vention machines, packaged as one plugin, `vention`, in the `vention-skills` marketplace.
4
4
 
5
5
  | Skill | What it does |
6
6
  | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vention/vention-skills",
3
- "version": "0.10.15",
3
+ "version": "0.10.16",
4
4
  "description": "Vention skills for coding agents: write MachineLogic applications, build machine designs, and diagnose deployed machines.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "vention",
4
- "version": "0.10.15",
4
+ "version": "0.10.16",
5
5
  "description": "Vention skills for coding agents: write MachineLogic applications, build machine designs, and diagnose deployed machines.",
6
6
  "author": {
7
7
  "name": "Vention"
@@ -3,7 +3,7 @@ name: vention-design
3
3
  description: Create, build and edit Vention machine designs and design graphs for agentic layout. Use when the user wants to start a new design, lay out an automation cell, add or reposition components in a Vention design, generate a machine layout from a description, add or place a floor plan image under a design, list a design's versions, or inspect an existing design graph. Trigger on MachineBuilder designs, design graphs, assembly graphs, extrusions, actuators, robot mounting, cell footprints, floor plans, or a machine-builder URL.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.10.15"
6
+ version: "0.10.16"
7
7
  author: Vention
8
8
  ---
9
9
 
@@ -3,7 +3,7 @@ name: vention-machine-logic
3
3
  description: Write, edit, validate, and deploy MachineLogic Python applications for Vention machines with the machine-logic-sdk and the Vention CLI. Load it before writing or changing any Python that talks to a Vention machine, however short, since device names and version-specific SDK calls come from here. Use to program a Vention machine or MachineMotion controller, drive actuators, conveyors, robots, pneumatics, or digital I/O, change an existing application, build a state machine, debug from logs, or pull, push, or link with the Vention CLI (vention or vn). Trigger on MachineLogic, machine-logic-sdk, MachineMotion, MachineBuilder applications, the Vention CLI, or a directory holding .machine-code-app-directory-info.json. Printing what the machine or robot holds now (positions, taught targets, calibration, I/O) is a program: files on disk hold only as-designed values. Not for explaining an existing machine or its configuration in words when no program is asked for, nor for Python that never touches the machine.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.10.15"
6
+ version: "0.10.16"
7
7
  machine-logic-sdk: ">=3.0,<4"
8
8
  author: Vention
9
9
  ---
@@ -3,7 +3,7 @@ name: vention-machine-logic-hmi
3
3
  description: Build or edit the custom operator HMI of a MachineLogic application, the React touch screen in customui/ that runs in MachineBuilder and on the MachineMotion pendant, wired to the application's Python process through vention-communication. Use when the user asks for an operator screen, a custom HMI or UI, a touch panel, start and stop buttons, live machine state on screen, or recipes an operator picks, for a Vention machine or inside a directory holding .machine-code-app-directory-info.json. Uses @vention/machine-ui, @vention/machine-apps-components and @vention/machine-logic-ui-sdk. Do not use for the no-code HMI Builder, or for the Python motion logic alone (that is vention-machine-logic).
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.10.15"
6
+ version: "0.10.16"
7
7
  author: Vention
8
8
  ---
9
9
 
@@ -165,7 +165,7 @@ The machine-apps-components README is behind its code: `NavigationBar` takes `Na
165
165
  - Confirm anything that starts motion or deletes data in a `VentionModal` with `isTouchDevice`, and say in it what will move.
166
166
  - `VentionButton` variants are `filled-brand`, `filled`, `outline`, `text-only`, `shaded` and `destructive`. MUI's `contained` and `outlined` do not exist on it, and neither does a `color` prop.
167
167
  - Start is green and Stop is red, both set through the `StatusTopBar` button's `backgroundColor`, `backgroundColorHover` and `textColor`.
168
- - A `LogsPanel` row cuts `message` and `description` to one line and shows the rest only on hover. Keep `message` to a few words saying what happened, put the cause and what to do next in `description`, and open the whole entry in a `VentionModal` from `onLogClick`. Raise every error event as a banner on whatever page is open, whose View button opens that entry on the Logs page, as the example's `app.tsx` does.
168
+ - A `LogsPanel` row cuts `message` and `description` to one line and shows the rest only on hover. Keep `message` to a few words saying what happened, put the cause and what to do next in `description`, and open the whole entry in a `VentionModal` from `onLogClick`. Log the HMI's own command failures there too, beside the server's events. Raise every error as a banner on whatever page is open, whose View button opens that entry on the Logs page; the banner fades after a few seconds, and the Logs tab shows a red count of the errors logged since that page was last opened, as the example's `app.tsx` does. Alerts float over the page just under the `StatusTopBar`, never in the page's flow, so one appearing never moves the layout.
169
169
 
170
170
  ## Validate before you push
171
171
 
@@ -180,7 +180,7 @@ The HMI drives physical machinery that can injure someone.
180
180
 
181
181
  - Start never fires on page load, on reconnect, or from a stream message. Only an operator's tap, confirmed in a modal, starts motion.
182
182
  - Start and Stop live in the `StatusTopBar`, so they are on every page. Do not repeat them on a page. Raise the bar above `theme.zIndex.modal`, as the example's `app.tsx` does, or an open `VentionModal` covers Stop.
183
- - Stop is disabled until this page load has received its first status, since no Stop can reach the app before then; show why next to it and say to use the emergency stop if the machine moves. After that, Stop stays enabled unless the station is known to be ready or already stopping, as the example's `canStop` does. A fault is neither, so Stop stays enabled there, and so it does while the stream is disconnected: a dropped stream may hide a moving machine. When Stop fails, say it did not reach the station and to use the emergency stop. Every other command is disabled while disconnected.
183
+ - Stop is disabled until this page load has received its first status, since no Stop can reach the app before then; show why next to it and say to use the emergency stop if the machine moves. After that, Stop stays enabled unless the station is known to be ready or already stopping, as the example's `canStop` does. A fault is neither, so Stop stays enabled there, and so it does while the stream is disconnected: a dropped stream may hide a moving machine. When Stop fails, say it did not reach the station and to use the emergency stop, in an alert that stays until the operator closes it. Every other command is disabled while disconnected.
184
184
  - A tap on Stop shows at once. While the station stops, the Stop button reads "Stopping…" (derive that label from the streamed state and `connected`, as the example's `isStopping(status, connected)` does, never from a tapped flag alone: once the stream drops it reads "Stop" again and stays pressable), the Operate page's progress bar marks where the run now ends (the cycle in progress amber, the cancelled ones hatched and struck through), and a line by the heading says the emergency stop halts at once. Say it on the screen the operator already watches rather than in a separate alert.
185
185
  - Send Start with a `timeoutMs` and an abort signal, and abort it when the stream drops. A Start the operator can no longer see must not land later; say it was not confirmed and to check the station.
186
186
  - The HMI never offers a way around an emergency stop, safety interlock, light curtain or guard, not even a hidden one for testing.
@@ -2,9 +2,9 @@ import { Box } from "@mui/material"
2
2
  import { IconBook, IconHistory, IconLayoutDashboard, IconPlayerPlayFilled, IconPlayerStopFilled } from "@tabler/icons-react"
3
3
  import { NavigationBar, StatusTopBar } from "@vention/machine-apps-components"
4
4
  import type { LogEntry, LogType } from "@vention/machine-apps-components"
5
- import { COLORS, VentionAlert, VentionModal } from "@vention/machine-ui"
6
- import { useEffect, useRef, useState } from "react"
7
- import { Navigate, Route, Routes, useNavigate } from "react-router-dom"
5
+ import { COLORS, VentionAlert, VentionCounter, VentionModal } from "@vention/machine-ui"
6
+ import { useCallback, useEffect, useRef, useState } from "react"
7
+ import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
8
8
  import { client, openEvents, openStatus, STATUS_STALL_MS } from "./client"
9
9
  import type { Event, Recipe } from "./gen/app_pb"
10
10
  import { LogsPage } from "./pages/logs-page"
@@ -19,6 +19,8 @@ const NAV_ICON_SIZE = 42
19
19
  const TOP_BAR_ICON_SIZE = 32
20
20
  // A Start that has not been answered by then is abandoned rather than left to land later.
21
21
  const START_TIMEOUT_MS = 5000
22
+ // The Logs tab's unread count keeps an error after its banner fades.
23
+ const ERROR_BANNER_MS = 8000
22
24
 
23
25
  function toLogLevel(level: string): LogType {
24
26
  if (level === "error") return "error"
@@ -26,37 +28,40 @@ function toLogLevel(level: string): LogType {
26
28
  return "info"
27
29
  }
28
30
 
29
- function useEventLog(): LogEntry[] {
31
+ /** The server's events, plus the HMI's own command failures, newest first. */
32
+ function useEventLog(): { entries: LogEntry[]; addEntry: (level: LogType, message: string, description: string) => string } {
30
33
  const { value: latestEvent } = useStream<Event>(openEvents)
31
34
  const [entries, setEntries] = useState<LogEntry[]>([])
32
35
  const nextId = useRef(0)
33
36
 
34
- useEffect(() => {
35
- if (latestEvent === undefined) return
37
+ const addEntry = useCallback((level: LogType, message: string, description: string, date = new Date().toISOString()) => {
36
38
  nextId.current += 1
37
- const entry: LogEntry = {
38
- id: String(nextId.current),
39
- date: latestEvent.timestamp,
40
- level: toLogLevel(latestEvent.level),
41
- code: latestEvent.level.toUpperCase(),
42
- message: latestEvent.message,
43
- description: latestEvent.detail,
44
- }
39
+ const entry: LogEntry = { id: String(nextId.current), date, level, code: level.toUpperCase(), message, description }
45
40
  setEntries(previous => [entry, ...previous].slice(0, MAX_LOG_ENTRIES))
46
- }, [latestEvent])
41
+ return entry.id
42
+ }, [])
43
+
44
+ useEffect(() => {
45
+ if (latestEvent !== undefined) addEntry(toLogLevel(latestEvent.level), latestEvent.message, latestEvent.detail, latestEvent.timestamp)
46
+ }, [latestEvent, addEntry])
47
47
 
48
- return entries
48
+ return { entries, addEntry }
49
49
  }
50
50
 
51
51
  export function App() {
52
52
  const { value: status, connected } = useStream(openStatus, STATUS_STALL_MS)
53
- const logEntries = useEventLog()
54
- const [commandError, setCommandError] = useState("")
53
+ const { entries: logEntries, addEntry } = useEventLog()
55
54
  const navigate = useNavigate()
56
- // Every error event raises a banner on whatever page is open; View opens the whole entry on the Logs page.
55
+ const isOnLogsPage = useLocation().pathname === "/logs"
56
+ // The latest error shows as a banner on every page until it fades; View opens the whole entry on the Logs page.
57
57
  const [errorEntry, setErrorEntry] = useState<LogEntry | undefined>(undefined)
58
58
  const [openLogEntry, setOpenLogEntry] = useState<LogEntry | undefined>(undefined)
59
+ // A Stop that did not land never fades: the operator must know to use the emergency stop.
60
+ const [stopFailure, setStopFailure] = useState("")
61
+ const stopFailureIds = useRef(new Set<string>())
62
+ const [lastSeenLogId, setLastSeenLogId] = useState(0)
59
63
  const latestEntry = logEntries[0]
64
+ const unseenErrors = logEntries.filter(entry => entry.level === "error" && Number(entry.id) > lastSeenLogId).length
60
65
  const state = describeState(status, connected)
61
66
 
62
67
  const [recipe, setRecipe] = useState<Recipe | undefined>(undefined)
@@ -64,14 +69,26 @@ export function App() {
64
69
  const startRequest = useRef<AbortController | undefined>(undefined)
65
70
  const startable = canStart(status, connected) && recipe !== undefined
66
71
 
72
+ const reportError = useCallback((message: string, detail: string) => void addEntry("error", message, detail), [addEntry])
73
+
67
74
  useEffect(() => {
68
75
  if (!connected) startRequest.current?.abort()
69
76
  }, [connected])
70
77
 
71
78
  useEffect(() => {
72
- if (latestEntry?.level === "error") setErrorEntry(latestEntry)
79
+ if (latestEntry?.level === "error" && !stopFailureIds.current.has(latestEntry.id)) setErrorEntry(latestEntry)
73
80
  }, [latestEntry])
74
81
 
82
+ useEffect(() => {
83
+ if (errorEntry === undefined) return
84
+ const timer = setTimeout(() => setErrorEntry(undefined), ERROR_BANNER_MS)
85
+ return () => clearTimeout(timer)
86
+ }, [errorEntry])
87
+
88
+ useEffect(() => {
89
+ if (isOnLogsPage && latestEntry !== undefined) setLastSeenLogId(Number(latestEntry.id))
90
+ }, [isOnLogsPage, latestEntry])
91
+
75
92
  const viewError = () => {
76
93
  setOpenLogEntry(errorEntry)
77
94
  setErrorEntry(undefined)
@@ -85,16 +102,18 @@ export function App() {
85
102
  startRequest.current = controller
86
103
  try {
87
104
  const response = await client.start({ recipeId: recipe.id }, { signal: controller.signal, timeoutMs: START_TIMEOUT_MS })
88
- if (!response.accepted) setCommandError(`Start refused: ${response.message}`)
105
+ if (!response.accepted) reportError("Start refused", response.message)
89
106
  } catch (error) {
90
- setCommandError(`Start not confirmed: ${errorText(error)}. Check the station state before starting again.`)
107
+ reportError("Start not confirmed", `${errorText(error)}. Check the station state before starting again.`)
91
108
  }
92
109
  }
93
110
 
94
111
  const stop = () => {
95
- client
96
- .trigger_Stop({})
97
- .catch((error: unknown) => setCommandError(`Stop did not reach the station (${errorText(error)}). Use the emergency stop.`))
112
+ client.trigger_Stop({}).catch((error: unknown) => {
113
+ const detail = errorText(error)
114
+ stopFailureIds.current.add(addEntry("error", "Stop did not reach the station", `${detail}. Use the emergency stop.`))
115
+ setStopFailure(`Stop did not reach the station (${detail}). Use the emergency stop.`)
116
+ })
98
117
  }
99
118
 
100
119
  return (
@@ -127,47 +146,64 @@ export function App() {
127
146
  },
128
147
  ]}
129
148
  />
149
+ {/* Alerts float over the page, so one appearing never moves the page; the gaps between them stay tappable. */}
150
+ <Box
151
+ sx={{
152
+ position: "absolute",
153
+ top: "100%",
154
+ left: 0,
155
+ right: 0,
156
+ padding: 4,
157
+ display: "flex",
158
+ flexDirection: "column",
159
+ gap: 2,
160
+ pointerEvents: "none",
161
+ "& > *": { pointerEvents: "auto" },
162
+ }}
163
+ >
164
+ {status === undefined && (
165
+ <VentionAlert
166
+ severity="warn"
167
+ size="xx-large"
168
+ title="Not connected to the app yet"
169
+ descriptionText="Stop is available once the app answers. Use the emergency stop if the machine moves."
170
+ isFullWidth
171
+ appearance="elevated"
172
+ />
173
+ )}
174
+ {errorEntry !== undefined && (
175
+ <VentionAlert
176
+ severity="error"
177
+ size="xx-large"
178
+ title={errorEntry.message}
179
+ primaryButtonText="View"
180
+ onPrimaryButtonClick={viewError}
181
+ isFullWidth
182
+ appearance="elevated"
183
+ isCloseable
184
+ onCloseButtonClick={() => setErrorEntry(undefined)}
185
+ />
186
+ )}
187
+ {stopFailure !== "" && (
188
+ <VentionAlert
189
+ severity="error"
190
+ size="xx-large"
191
+ title={stopFailure}
192
+ isFullWidth
193
+ appearance="elevated"
194
+ isCloseable
195
+ onCloseButtonClick={() => setStopFailure("")}
196
+ />
197
+ )}
198
+ </Box>
130
199
  </Box>
131
- {status === undefined && (
132
- <VentionAlert
133
- severity="warn"
134
- size="xx-large"
135
- title="Not connected to the app yet"
136
- descriptionText="Stop is available once the app answers. Use the emergency stop if the machine moves."
137
- isFullWidth
138
- />
139
- )}
140
- {errorEntry !== undefined && (
141
- <VentionAlert
142
- severity="error"
143
- size="xx-large"
144
- title={errorEntry.message}
145
- primaryButtonText="View"
146
- onPrimaryButtonClick={viewError}
147
- isFullWidth
148
- isCloseable
149
- onCloseButtonClick={() => setErrorEntry(undefined)}
150
- />
151
- )}
152
- {commandError !== "" && (
153
- <VentionAlert
154
- severity="error"
155
- size="xx-large"
156
- title={commandError}
157
- isFullWidth
158
- isCloseable
159
- onCloseButtonClick={() => setCommandError("")}
160
- />
161
- )}
162
200
  <Box sx={{ flex: 1, minHeight: 0, overflow: "auto", padding: 6 }}>
163
201
  <Routes>
164
202
  <Route
165
203
  path="/"
166
- element={
167
- <OperatePage status={status} connected={connected} recipe={recipe} onRecipeChange={setRecipe} onError={setCommandError} />
168
- }
204
+ element={<OperatePage status={status} connected={connected} recipe={recipe} onRecipeChange={setRecipe} onError={reportError} />}
169
205
  />
170
- <Route path="/recipes" element={<RecipesPage connected={connected} onError={setCommandError} />} />
206
+ <Route path="/recipes" element={<RecipesPage connected={connected} onError={reportError} />} />
171
207
  <Route path="/logs" element={<LogsPage entries={logEntries} openEntry={openLogEntry} onOpenEntry={setOpenLogEntry} />} />
172
208
  <Route path="*" element={<Navigate to="/" replace />} />
173
209
  </Routes>
@@ -175,7 +211,22 @@ export function App() {
175
211
  <NavigationBar variant="flex">
176
212
  <NavigationBar.Item id="operate" label="Operate" path="/" icon={<IconLayoutDashboard size={NAV_ICON_SIZE} color="white" />} />
177
213
  <NavigationBar.Item id="recipes" label="Recipes" path="/recipes" icon={<IconBook size={NAV_ICON_SIZE} color="white" />} />
178
- <NavigationBar.Item id="logs" label="Logs" path="/logs" icon={<IconHistory size={NAV_ICON_SIZE} color="white" />} />
214
+ <NavigationBar.Item
215
+ id="logs"
216
+ label="Logs"
217
+ path="/logs"
218
+ icon={
219
+ // NavigationBar.Item has no badge, so the unread count sits on the icon's corner.
220
+ <Box sx={{ position: "relative", display: "flex" }}>
221
+ <IconHistory size={NAV_ICON_SIZE} color="white" />
222
+ {unseenErrors > 0 && (
223
+ <Box sx={{ position: "absolute", top: -8, right: -12 }}>
224
+ <VentionCounter color="red" variant="filled" size="large" value={unseenErrors} />
225
+ </Box>
226
+ )}
227
+ </Box>
228
+ }
229
+ />
179
230
  </NavigationBar>
180
231
  <VentionModal
181
232
  isOpen={confirmingStart && startable}
@@ -10,7 +10,7 @@ interface OperatePageProps {
10
10
  connected: boolean
11
11
  recipe: Recipe | undefined
12
12
  onRecipeChange: (recipe: Recipe | undefined) => void
13
- onError: (message: string) => void
13
+ onError: (message: string, detail: string) => void
14
14
  }
15
15
 
16
16
  /** What the page shows. The same regions render in every phase; only their content changes. */
@@ -202,12 +202,12 @@ export function OperatePage({ status, connected, recipe, onRecipeChange, onError
202
202
  if (!connected) return
203
203
  client.recipe_ListRecords({}).then(
204
204
  response => setRecipes(response.records),
205
- (error: unknown) => onError(`Could not load recipes: ${errorText(error)}`)
205
+ (error: unknown) => onError("Could not load recipes", errorText(error))
206
206
  )
207
207
  }, [connected, onError])
208
208
 
209
209
  const reset = () => {
210
- client.trigger_Reset({}).catch((error: unknown) => onError(`Reset failed: ${errorText(error)}`))
210
+ client.trigger_Reset({}).catch((error: unknown) => onError("Reset failed", errorText(error)))
211
211
  }
212
212
 
213
213
  return (
@@ -19,7 +19,7 @@ const NUMERIC_FIELDS: { field: NumericField; label: string; unit: string; step:
19
19
 
20
20
  interface RecipesPageProps {
21
21
  connected: boolean
22
- onError: (message: string) => void
22
+ onError: (message: string, detail: string) => void
23
23
  }
24
24
 
25
25
  export function RecipesPage({ connected, onError }: RecipesPageProps) {
@@ -32,7 +32,7 @@ export function RecipesPage({ connected, onError }: RecipesPageProps) {
32
32
  try {
33
33
  setRecipes((await client.recipe_ListRecords({})).records)
34
34
  } catch (error) {
35
- onError(`Could not load recipes: ${errorText(error)}`)
35
+ onError("Could not load recipes", errorText(error))
36
36
  }
37
37
  }, [onError])
38
38
 
@@ -54,7 +54,7 @@ export function RecipesPage({ connected, onError }: RecipesPageProps) {
54
54
  setEditingId(response.record?.id)
55
55
  await reload()
56
56
  } catch (error) {
57
- onError(`Could not save ${draft.name}: ${errorText(error)}`)
57
+ onError(`Could not save ${draft.name}`, errorText(error))
58
58
  }
59
59
  }
60
60
 
@@ -66,7 +66,7 @@ export function RecipesPage({ connected, onError }: RecipesPageProps) {
66
66
  edit(undefined)
67
67
  await reload()
68
68
  } catch (error) {
69
- onError(`Could not delete ${draft.name}: ${errorText(error)}`)
69
+ onError(`Could not delete ${draft.name}`, errorText(error))
70
70
  }
71
71
  }
72
72
 
@@ -3,7 +3,7 @@ name: vention-monitoring
3
3
  description: Diagnose deployed Vention machines from controller state, history, health, and activity data. Use when the user reports a machine that is down, faulted, offline, slow, or underperforming, asks why a cycle time regressed, wants to interpret MachineMotion event data or drive and brake status, asks about OEE or uptime, or wants to know which machine in a fleet needs attention. Trigger on a MachineMotion serial number plus any complaint.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.10.15"
6
+ version: "0.10.16"
7
7
  author: Vention
8
8
  ---
9
9