@nwire/studio 0.10.0 → 0.11.0
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 +27 -16
- package/package.json +2 -2
- package/src/App.vue +142 -27
- package/src/components/SourceDrawer.vue +46 -9
- package/src/components/SourcePill.vue +18 -53
- package/src/lib/__tests__/normalize-cache.test.ts +6 -5
- package/src/lib/cache.ts +60 -82
- package/src/lib/normalize-cache.ts +1 -1
- package/src/lib/project-catalog.ts +39 -1
- package/src/main.ts +52 -16
- package/src/pages/Actions.vue +5 -14
- package/src/pages/Apps.vue +177 -0
- package/src/pages/Dispatch.vue +4 -4
- package/src/pages/Events.vue +84 -40
- package/src/pages/Home.vue +133 -19
- package/src/pages/Hooks.vue +3 -8
- package/src/pages/Overview.vue +6 -4
- package/src/pages/Plugins.vue +3 -8
- package/src/pages/Projections.vue +148 -0
- package/src/pages/Projects.vue +2 -2
- package/src/pages/Queries.vue +148 -0
- package/src/pages/Run.vue +144 -5
- package/src/pages/Sinks.vue +124 -0
- package/src/pages/Topology.vue +91 -91
- package/src/pages/Trace.vue +2 -21
- package/src/pages/TraceNode.vue +2 -4
- package/src/pages/Workflows.vue +19 -26
- package/src/pages/__tests__/Projections.test.ts +90 -0
- package/src/pages/__tests__/Queries.test.ts +86 -0
- package/vite.config.ts +275 -34
- package/src/pages/Modules.vue +0 -174
package/README.md
CHANGED
|
@@ -31,22 +31,33 @@ supervisor, dispatch).
|
|
|
31
31
|
|
|
32
32
|
## Pages
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
| Run
|
|
34
|
+
The nav is grouped into **Map** (the live system), **Run** (operate
|
|
35
|
+
it), **Inspect** (browse the static surface). Screenshots are
|
|
36
|
+
captured live against `examples/moderation-queue` — see
|
|
37
|
+
[docs/concepts/studio](../../docs/concepts/studio.md) for the
|
|
38
|
+
annotated tour.
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
| Group | Page | Reads | Renders |
|
|
43
|
+
| ------- | ----------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| | Projects | localStorage catalog | Catalog of every workspace Studio has ever opened. |
|
|
45
|
+
| | Home | `manifest.json` + live telemetry | Per-project dashboard — recent failures, boot summary, composition stats. |
|
|
46
|
+
| Map | Topology | manifest + `graph.events` | VueFlow graph — apps as nodes with their plugins + sinks inline, cross-app event edges. |
|
|
47
|
+
| Map | Trace | `/__nwire/telemetry/stream` (SSE) | Causation tree per `correlationId`; Play Trace replays telemetry across canvas with amber-glow per sticky. |
|
|
48
|
+
| Map | Stream | `/__nwire/events/stream` (SSE) | Live event firehose. |
|
|
49
|
+
| Run | Try | `/__nwire/dispatch` | Form-from-Zod-schema action dispatch against the live wire. |
|
|
50
|
+
| Run | Processes | `/__nwire/run/*` (supervisor) | Start dev with custom port + env, stream stdout, recognise external `nwire dev` processes via `.nwire/processes/*`. |
|
|
51
|
+
| Run | Commands | `/__nwire/run/commands` | Operator `defineCommand` entries with run buttons. |
|
|
52
|
+
| Run | Workflows | `workflows` | Workflow defs with subscribed events and dispatched actions. |
|
|
53
|
+
| Inspect | Apps | `apps` + `plugins` + `sinks` | Every App with its plugin stack, primitive counts, and outbound sinks. |
|
|
54
|
+
| Inspect | Actions | `actions` | Searchable list + detail panel: schema, retry, persona, journey, SLO, source link. |
|
|
55
|
+
| Inspect | Events | `events` + `graph.events` | Catalog with the `.public()` gate badge + per-event producer/consumer flow. |
|
|
56
|
+
| Inspect | Projections | `projections` | CQRS read models; each fold with the events it listens to and the queries reading it. |
|
|
57
|
+
| Inspect | Queries | `queries` | Read endpoints — projection-backed or direct-handler. |
|
|
58
|
+
| Inspect | Sinks | `sinks` | Outbound delivery chain — every stage by position (early / middle / terminal). |
|
|
59
|
+
| Inspect | Plugins | `plugins` | Installed plugins per app — bundle-mode forge, sub-plugins, custom. |
|
|
60
|
+
| Inspect | Hooks | `hooks` | Every materialised framework hook slot and its chain length. |
|
|
50
61
|
|
|
51
62
|
## Surface (programmatic)
|
|
52
63
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nwire/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Nwire Studio — visual companion for the framework. Vue 3 + Vite + shadcn-vue + VueFlow. System topology, action runner, actor browser, projection viewer, DLQ inspector.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"devtools",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"vite": "npm:rolldown-vite@latest",
|
|
41
41
|
"vue": "^3.5.13",
|
|
42
42
|
"vue-router": "^4.5.0",
|
|
43
|
-
"@nwire/supervisor": "0.
|
|
43
|
+
"@nwire/supervisor": "0.11.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@playwright/test": "^1.60.0",
|
package/src/App.vue
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { onMounted, onBeforeUnmount, ref, watch, computed } from "vue";
|
|
3
|
-
import { RouterLink, RouterView, useRoute } from "vue-router";
|
|
3
|
+
import { RouterLink, RouterView, useRoute, useRouter } from "vue-router";
|
|
4
4
|
import {
|
|
5
5
|
upsertCurrent,
|
|
6
6
|
loadCatalog,
|
|
7
7
|
getActiveProjectCwd,
|
|
8
8
|
setActiveProjectCwd,
|
|
9
|
+
setActiveSlug,
|
|
10
|
+
projectSlug,
|
|
11
|
+
cwdForSlug,
|
|
9
12
|
type ProjectSnapshot,
|
|
10
13
|
} from "@/lib/project-catalog";
|
|
11
14
|
import {
|
|
@@ -24,14 +27,58 @@ import {
|
|
|
24
27
|
Anchor,
|
|
25
28
|
Puzzle,
|
|
26
29
|
FolderOpen,
|
|
30
|
+
Map,
|
|
31
|
+
Wrench,
|
|
32
|
+
Eye,
|
|
33
|
+
Database,
|
|
34
|
+
Search,
|
|
35
|
+
Terminal,
|
|
27
36
|
} from "lucide-vue-next";
|
|
28
37
|
import { useCache } from "@/lib/cache";
|
|
29
38
|
import { ErrorBoundary } from "@/components";
|
|
30
39
|
import { Button } from "@/components/ui/button";
|
|
31
40
|
|
|
32
41
|
const route = useRoute();
|
|
42
|
+
const router = useRouter();
|
|
33
43
|
const { cache, loading, error, missingFields, reload } = useCache();
|
|
34
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The active project slug — read from the URL when present, falls back
|
|
47
|
+
* to the launched project's slug. Drives every sidebar link so all
|
|
48
|
+
* navigation stays inside the project.
|
|
49
|
+
*/
|
|
50
|
+
const activeSlug = computed(() => {
|
|
51
|
+
const fromRoute = route.params.slug as string | undefined;
|
|
52
|
+
if (fromRoute) return fromRoute;
|
|
53
|
+
return project.value ? projectSlug(project.value) : null;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** Build a project-scoped router path. */
|
|
57
|
+
function pPath(suffix: string): string {
|
|
58
|
+
const slug = activeSlug.value;
|
|
59
|
+
if (!slug) return suffix;
|
|
60
|
+
const tail = suffix === "/" ? "" : suffix;
|
|
61
|
+
return `/projects/${slug}${tail}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* When the URL slug changes (route navigation, manual paste, switch),
|
|
66
|
+
* resolve it to a cwd via the catalog and pin both legacy keys so the
|
|
67
|
+
* fetch shim + project-status middleware target the right project.
|
|
68
|
+
*/
|
|
69
|
+
watch(
|
|
70
|
+
() => route.params.slug,
|
|
71
|
+
(slug) => {
|
|
72
|
+
if (typeof slug !== "string" || !slug) return;
|
|
73
|
+
const cwd = cwdForSlug(slug);
|
|
74
|
+
if (cwd) {
|
|
75
|
+
setActiveProjectCwd(cwd);
|
|
76
|
+
setActiveSlug(slug);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{ immediate: true },
|
|
80
|
+
);
|
|
81
|
+
|
|
35
82
|
// Project identity — multi-project (Shape A). The active project is
|
|
36
83
|
// stored in localStorage; the server uses `?project=<cwd>` (injected via
|
|
37
84
|
// the fetch shim in main.ts) to scope every endpoint. We need to register
|
|
@@ -42,18 +89,29 @@ const catalog = ref<Record<string, ProjectSnapshot>>({});
|
|
|
42
89
|
|
|
43
90
|
async function bootProjects() {
|
|
44
91
|
// Server-side registration happens in main.ts before mount (so the
|
|
45
|
-
// cache fetch has an accepted cwd). Here we
|
|
92
|
+
// cache fetch has an accepted cwd). Here we resolve the active
|
|
46
93
|
// project's metadata + load the catalog into reactive state.
|
|
47
94
|
catalog.value = loadCatalog();
|
|
95
|
+
// If the URL carries a /projects/:slug, that's the source of truth —
|
|
96
|
+
// resolve it against the catalog and pin the cwd before any fetch.
|
|
97
|
+
const slugFromRoute = route.params.slug as string | undefined;
|
|
98
|
+
if (slugFromRoute) {
|
|
99
|
+
const cwd = cwdForSlug(slugFromRoute);
|
|
100
|
+
if (cwd) {
|
|
101
|
+
setActiveProjectCwd(cwd);
|
|
102
|
+
setActiveSlug(slugFromRoute);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
48
105
|
const stored = getActiveProjectCwd();
|
|
49
106
|
try {
|
|
50
107
|
const res = await fetch("/__nwire/project");
|
|
51
108
|
if (res.ok) {
|
|
52
109
|
const body = (await res.json()) as { name: string; cwd: string };
|
|
53
110
|
project.value = body;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
111
|
+
if (!stored) {
|
|
112
|
+
setActiveProjectCwd(body.cwd);
|
|
113
|
+
setActiveSlug(projectSlug(body));
|
|
114
|
+
}
|
|
57
115
|
}
|
|
58
116
|
} catch {
|
|
59
117
|
// header just shows "Nwire Studio" if the fetch fails
|
|
@@ -66,11 +124,15 @@ const catalogList = computed<ProjectSnapshot[]>(() =>
|
|
|
66
124
|
);
|
|
67
125
|
|
|
68
126
|
function switchProject(cwd: string) {
|
|
127
|
+
const snap = catalog.value[cwd];
|
|
128
|
+
const slug = snap ? projectSlug(snap) : null;
|
|
69
129
|
setActiveProjectCwd(cwd);
|
|
130
|
+
if (slug) setActiveSlug(slug);
|
|
70
131
|
// Hard reload — every page's data depends on the active project, and
|
|
71
132
|
// SSE streams are tied to the old one. A fresh page guarantees a clean
|
|
72
|
-
// pivot without untangling every page's reactive subscriptions.
|
|
73
|
-
|
|
133
|
+
// pivot without untangling every page's reactive subscriptions. Land
|
|
134
|
+
// on the new project's Home so the URL matches the pinned project.
|
|
135
|
+
window.location.assign(slug ? `/projects/${slug}` : "/projects");
|
|
74
136
|
}
|
|
75
137
|
|
|
76
138
|
// ─── Project switcher dropdown — click-driven + close-on-outside ────
|
|
@@ -110,6 +172,24 @@ onBeforeUnmount(() => {
|
|
|
110
172
|
// the cache does, so we watch both.
|
|
111
173
|
watch([project, cache], persistSnapshot);
|
|
112
174
|
|
|
175
|
+
/**
|
|
176
|
+
* One-shot URL upgrade — if the user landed on a bare path like
|
|
177
|
+
* `/actions` we rewrite to `/projects/<slug>/actions` once the slug
|
|
178
|
+
* resolves. Lets old bookmarks and CI suites keep working while
|
|
179
|
+
* preserving the project-pinned canonical URL going forward.
|
|
180
|
+
*/
|
|
181
|
+
watch([project, () => route.path], ([_, currentPath]) => {
|
|
182
|
+
if (!project.value) return;
|
|
183
|
+
const slug = projectSlug(project.value);
|
|
184
|
+
if (currentPath.startsWith("/projects/")) return;
|
|
185
|
+
if (currentPath === "/projects") return;
|
|
186
|
+
// Don't fight a fresh navigation in flight.
|
|
187
|
+
const target = currentPath === "/" ? `/projects/${slug}` : `/projects/${slug}${currentPath}`;
|
|
188
|
+
if (target !== currentPath) {
|
|
189
|
+
void router.replace(target);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
113
193
|
function persistSnapshot() {
|
|
114
194
|
if (!project.value) return;
|
|
115
195
|
const c = cache.value;
|
|
@@ -120,7 +200,7 @@ function persistSnapshot() {
|
|
|
120
200
|
composition: c
|
|
121
201
|
? {
|
|
122
202
|
apps: c.apps.length,
|
|
123
|
-
|
|
203
|
+
plugins: c.plugins.length,
|
|
124
204
|
actions: c.actions.length,
|
|
125
205
|
events: c.events.length,
|
|
126
206
|
resolvers: c.resolvers?.length ?? 0,
|
|
@@ -141,36 +221,53 @@ async function rebuildCache() {
|
|
|
141
221
|
setTimeout(() => void reload(), 800);
|
|
142
222
|
}
|
|
143
223
|
|
|
144
|
-
// Studio nav —
|
|
145
|
-
//
|
|
146
|
-
//
|
|
224
|
+
// Studio nav — three top-level surfaces:
|
|
225
|
+
//
|
|
226
|
+
// Map — the live system in motion (architecture, trace, stream)
|
|
227
|
+
// Run — operate the system (dispatch actions, drive processes, run CLI)
|
|
228
|
+
// Inspect — browse the static surface (every primitive that's wired up)
|
|
229
|
+
//
|
|
230
|
+
// Home + Projects sit above the groups: Home is the per-project dashboard;
|
|
231
|
+
// Projects is the catalog of every workspace Studio has ever opened.
|
|
232
|
+
const topItems = [
|
|
233
|
+
{ to: "/projects", label: "Projects", icon: FolderOpen },
|
|
234
|
+
{ to: "/", label: "Home", icon: Home },
|
|
235
|
+
];
|
|
236
|
+
|
|
147
237
|
const navGroups = [
|
|
148
238
|
{
|
|
149
|
-
label: "
|
|
239
|
+
label: "Map",
|
|
240
|
+
icon: Map,
|
|
150
241
|
items: [
|
|
151
|
-
{ to: "/
|
|
152
|
-
{ to: "/", label: "Home", icon: Home },
|
|
242
|
+
{ to: "/topology", label: "Topology", icon: Network }, // app+module shape
|
|
153
243
|
{ to: "/trace", label: "Trace", icon: Workflow }, // causal tree, one correlationId
|
|
154
244
|
{ to: "/live", label: "Stream", icon: Waves }, // raw live event firehose
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
label: "Run",
|
|
249
|
+
icon: Wrench,
|
|
250
|
+
items: [
|
|
155
251
|
{ to: "/dispatch", label: "Try", icon: Send }, // form-driven action dispatch
|
|
156
|
-
{ to: "/run", label: "Processes", icon: Play }, // start/stop
|
|
252
|
+
{ to: "/run", label: "Processes", icon: Play }, // start/stop processes + logs
|
|
253
|
+
{ to: "/commands", label: "Commands", icon: Terminal }, // please CLI surface
|
|
254
|
+
{ to: "/workflows", label: "Workflows", icon: GitBranch }, // workflow defs + runs
|
|
157
255
|
],
|
|
158
256
|
},
|
|
159
257
|
{
|
|
160
|
-
label: "
|
|
258
|
+
label: "Inspect",
|
|
259
|
+
icon: Eye,
|
|
161
260
|
items: [
|
|
162
|
-
{ to: "/
|
|
261
|
+
{ to: "/apps", label: "Apps", icon: Boxes }, // bounded contexts — one per app
|
|
163
262
|
{ to: "/actions", label: "Actions", icon: Zap },
|
|
164
263
|
{ to: "/events", label: "Events", icon: Radio },
|
|
165
|
-
{ to: "/
|
|
264
|
+
{ to: "/projections", label: "Projections", icon: Database },
|
|
265
|
+
{ to: "/queries", label: "Queries", icon: Search },
|
|
266
|
+
{ to: "/sinks", label: "Sinks", icon: Waves }, // outbound delivery chain
|
|
166
267
|
{ to: "/plugins", label: "Plugins", icon: Puzzle },
|
|
167
268
|
{ to: "/hooks", label: "Hooks", icon: Anchor },
|
|
168
269
|
],
|
|
169
270
|
},
|
|
170
|
-
{
|
|
171
|
-
label: "Architecture",
|
|
172
|
-
items: [{ to: "/topology", label: "Topology", icon: Network }],
|
|
173
|
-
},
|
|
174
271
|
];
|
|
175
272
|
</script>
|
|
176
273
|
|
|
@@ -232,19 +329,37 @@ const navGroups = [
|
|
|
232
329
|
</div>
|
|
233
330
|
<div class="text-[10px] text-zinc-600 mt-0.5">v0 · OSS</div>
|
|
234
331
|
</div>
|
|
235
|
-
<nav class="flex-1 px-2 py-3 space-y-
|
|
332
|
+
<nav class="flex-1 px-2 py-3 space-y-4 overflow-y-auto">
|
|
333
|
+
<div class="space-y-0.5">
|
|
334
|
+
<RouterLink
|
|
335
|
+
v-for="item in topItems"
|
|
336
|
+
:key="item.to"
|
|
337
|
+
:to="item.to === '/projects' ? '/projects' : pPath(item.to)"
|
|
338
|
+
class="flex items-center gap-2 px-2 py-1.5 rounded text-sm text-zinc-400 hover:bg-zinc-900 hover:text-zinc-100 transition-colors"
|
|
339
|
+
:class="{
|
|
340
|
+
'bg-zinc-900 text-zinc-100':
|
|
341
|
+
route.path === (item.to === '/projects' ? '/projects' : pPath(item.to)),
|
|
342
|
+
}"
|
|
343
|
+
>
|
|
344
|
+
<component :is="item.icon" class="w-4 h-4" />
|
|
345
|
+
{{ item.label }}
|
|
346
|
+
</RouterLink>
|
|
347
|
+
</div>
|
|
236
348
|
<div v-for="group in navGroups" :key="group.label">
|
|
237
|
-
<div
|
|
349
|
+
<div
|
|
350
|
+
class="px-2 pb-1 text-[10px] font-semibold uppercase tracking-wider text-zinc-600 flex items-center gap-1.5"
|
|
351
|
+
>
|
|
352
|
+
<component :is="group.icon" class="w-3 h-3" />
|
|
238
353
|
{{ group.label }}
|
|
239
354
|
</div>
|
|
240
355
|
<div class="space-y-0.5">
|
|
241
356
|
<RouterLink
|
|
242
357
|
v-for="item in group.items"
|
|
243
358
|
:key="item.to"
|
|
244
|
-
:to="item.to"
|
|
359
|
+
:to="pPath(item.to)"
|
|
245
360
|
class="flex items-center gap-2 px-2 py-1.5 rounded text-sm text-zinc-400 hover:bg-zinc-900 hover:text-zinc-100 transition-colors"
|
|
246
361
|
:class="{
|
|
247
|
-
'bg-zinc-900 text-zinc-100': route.path === item.to,
|
|
362
|
+
'bg-zinc-900 text-zinc-100': route.path === pPath(item.to),
|
|
248
363
|
}"
|
|
249
364
|
>
|
|
250
365
|
<component :is="item.icon" class="w-4 h-4" />
|
|
@@ -255,7 +370,7 @@ const navGroups = [
|
|
|
255
370
|
</nav>
|
|
256
371
|
<div class="border-t border-zinc-800 px-4 py-3 text-xs text-zinc-500 space-y-1">
|
|
257
372
|
<div v-if="cache" class="flex items-center justify-between">
|
|
258
|
-
<span>{{ cache.apps.length }} apps · {{ cache.
|
|
373
|
+
<span>{{ cache.apps.length }} apps · {{ cache.plugins.length }} plugins</span>
|
|
259
374
|
<button
|
|
260
375
|
class="text-zinc-400 hover:text-zinc-100 transition-colors"
|
|
261
376
|
title="Reload manifest"
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* returns `{ content, language }`. Permission scope is the repo root
|
|
12
12
|
* — the middleware refuses paths outside the cwd.
|
|
13
13
|
*/
|
|
14
|
-
import { ref, watch } from "vue";
|
|
15
|
-
import { X } from "lucide-vue-next";
|
|
14
|
+
import { computed, ref, watch } from "vue";
|
|
15
|
+
import { X, ExternalLink } from "lucide-vue-next";
|
|
16
16
|
import MonacoViewer from "./MonacoViewer.vue";
|
|
17
17
|
import SourcePill from "./SourcePill.vue";
|
|
18
18
|
|
|
@@ -22,6 +22,33 @@ const props = defineProps<{
|
|
|
22
22
|
|
|
23
23
|
const emit = defineEmits<{ (e: "close"): void }>();
|
|
24
24
|
|
|
25
|
+
const idePrefix = (): string => {
|
|
26
|
+
if (typeof localStorage === "undefined") return "vscode://file";
|
|
27
|
+
const ide = localStorage.getItem("nwire.ide") ?? "vscode";
|
|
28
|
+
switch (ide) {
|
|
29
|
+
case "cursor":
|
|
30
|
+
return "cursor://file";
|
|
31
|
+
case "zed":
|
|
32
|
+
return "zed://file";
|
|
33
|
+
case "idea":
|
|
34
|
+
case "webstorm":
|
|
35
|
+
case "jetbrains":
|
|
36
|
+
return `jetbrains://idea/navigate/reference?path=`;
|
|
37
|
+
default:
|
|
38
|
+
return "vscode://file";
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const ideHref = computed(() => {
|
|
43
|
+
if (!props.source) return undefined;
|
|
44
|
+
const { file, line, column } = props.source;
|
|
45
|
+
const prefix = idePrefix();
|
|
46
|
+
if (prefix.includes("?path=")) {
|
|
47
|
+
return `${prefix}${encodeURIComponent(file)}:${line}`;
|
|
48
|
+
}
|
|
49
|
+
return `${prefix}${file}:${line}${column ? `:${column}` : ""}`;
|
|
50
|
+
});
|
|
51
|
+
|
|
25
52
|
const content = ref<string>("");
|
|
26
53
|
const language = ref<string>("typescript");
|
|
27
54
|
const loading = ref(false);
|
|
@@ -77,13 +104,23 @@ watch(
|
|
|
77
104
|
<h2 class="text-sm font-medium text-zinc-200 truncate">Source</h2>
|
|
78
105
|
<SourcePill :source="source" compact />
|
|
79
106
|
</div>
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
107
|
+
<div class="flex items-center gap-2">
|
|
108
|
+
<a
|
|
109
|
+
v-if="ideHref"
|
|
110
|
+
:href="ideHref"
|
|
111
|
+
class="inline-flex items-center gap-1.5 rounded-md border border-zinc-800 bg-zinc-900/50 px-2 py-1 text-xs text-zinc-300 hover:text-orange-400 hover:border-orange-500/40 transition-colors"
|
|
112
|
+
>
|
|
113
|
+
<ExternalLink class="h-3 w-3" />
|
|
114
|
+
Open in IDE
|
|
115
|
+
</a>
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
class="text-zinc-500 hover:text-zinc-300 transition-colors"
|
|
119
|
+
@click="emit('close')"
|
|
120
|
+
>
|
|
121
|
+
<X class="h-4 w-4" />
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
87
124
|
</header>
|
|
88
125
|
|
|
89
126
|
<div class="flex-1 overflow-hidden p-3">
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* `SourcePill` — chip that shows a `file:line` source location
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `captureSourceLocation()`).
|
|
3
|
+
* `SourcePill` — chip that shows a `file:line` source location and emits
|
|
4
|
+
* `click` so the parent can open `SourceDrawer`. The IDE-open button
|
|
5
|
+
* lives inside the drawer; clicking the pill never bypasses the panel.
|
|
7
6
|
*
|
|
8
|
-
* <SourcePill :source="action.source" />
|
|
7
|
+
* <SourcePill :source="action.source" @click="sourcePreview = action.source" />
|
|
9
8
|
*
|
|
10
|
-
* The
|
|
11
|
-
*
|
|
12
|
-
* `idea`, `webstorm`. Users can change theirs from the Studio Overview
|
|
13
|
-
* "Settings" pane (see `pages/Overview.vue`).
|
|
9
|
+
* The copy button is still inline for quick `file:line` capture without
|
|
10
|
+
* opening the drawer.
|
|
14
11
|
*/
|
|
15
12
|
import { computed } from "vue";
|
|
16
|
-
import {
|
|
13
|
+
import { Copy, Check } from "lucide-vue-next";
|
|
17
14
|
import { useCopy } from "../composables/useCopy";
|
|
18
15
|
|
|
19
16
|
const props = defineProps<{
|
|
@@ -22,43 +19,16 @@ const props = defineProps<{
|
|
|
22
19
|
compact?: boolean;
|
|
23
20
|
}>();
|
|
24
21
|
|
|
22
|
+
defineEmits<{ (e: "click"): void }>();
|
|
23
|
+
|
|
25
24
|
const { copy, copied } = useCopy();
|
|
26
25
|
|
|
27
26
|
const fileSegment = computed(() => {
|
|
28
27
|
if (!props.source) return "";
|
|
29
28
|
const parts = props.source.file.split("/");
|
|
30
|
-
// Last 2 path segments give enough context without filling the chip.
|
|
31
29
|
return parts.slice(-2).join("/");
|
|
32
30
|
});
|
|
33
31
|
|
|
34
|
-
const idePrefix = (): string => {
|
|
35
|
-
if (typeof localStorage === "undefined") return "vscode://file";
|
|
36
|
-
const ide = localStorage.getItem("nwire.ide") ?? "vscode";
|
|
37
|
-
switch (ide) {
|
|
38
|
-
case "cursor":
|
|
39
|
-
return "cursor://file";
|
|
40
|
-
case "zed":
|
|
41
|
-
return "zed://file";
|
|
42
|
-
case "idea":
|
|
43
|
-
case "webstorm":
|
|
44
|
-
case "jetbrains":
|
|
45
|
-
return `jetbrains://idea/navigate/reference?path=`;
|
|
46
|
-
default:
|
|
47
|
-
return "vscode://file";
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const ideHref = computed(() => {
|
|
52
|
-
if (!props.source) return undefined;
|
|
53
|
-
const { file, line, column } = props.source;
|
|
54
|
-
const prefix = idePrefix();
|
|
55
|
-
// JetBrains uses a query-string scheme; everything else uses path:line:col.
|
|
56
|
-
if (prefix.includes("?path=")) {
|
|
57
|
-
return `${prefix}${encodeURIComponent(file)}:${line}`;
|
|
58
|
-
}
|
|
59
|
-
return `${prefix}${file}:${line}${column ? `:${column}` : ""}`;
|
|
60
|
-
});
|
|
61
|
-
|
|
62
32
|
const copyText = computed(() => {
|
|
63
33
|
if (!props.source) return "";
|
|
64
34
|
return `${props.source.file}:${props.source.line}${props.source.column ? `:${props.source.column}` : ""}`;
|
|
@@ -72,9 +42,12 @@ const onCopy = (e: Event) => {
|
|
|
72
42
|
</script>
|
|
73
43
|
|
|
74
44
|
<template>
|
|
75
|
-
<
|
|
45
|
+
<button
|
|
76
46
|
v-if="source"
|
|
77
|
-
|
|
47
|
+
type="button"
|
|
48
|
+
class="inline-flex items-center gap-1.5 rounded-md border border-zinc-800 bg-zinc-900/50 px-2 py-1 text-xs font-mono text-zinc-400 hover:border-zinc-700 hover:text-zinc-100 hover:bg-zinc-900 transition-colors"
|
|
49
|
+
:title="`Open ${copyText} in Studio's source panel`"
|
|
50
|
+
@click="$emit('click')"
|
|
78
51
|
>
|
|
79
52
|
<span
|
|
80
53
|
v-if="!compact"
|
|
@@ -82,22 +55,14 @@ const onCopy = (e: Event) => {
|
|
|
82
55
|
>
|
|
83
56
|
source
|
|
84
57
|
</span>
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
:title="`Open ${copyText} in IDE`"
|
|
88
|
-
class="inline-flex items-center gap-1 hover:text-orange-400"
|
|
89
|
-
>
|
|
90
|
-
{{ fileSegment }}:{{ source.line }}
|
|
91
|
-
<ExternalLink class="h-3 w-3 opacity-60" />
|
|
92
|
-
</a>
|
|
93
|
-
<button
|
|
94
|
-
type="button"
|
|
58
|
+
<span>{{ fileSegment }}:{{ source.line }}</span>
|
|
59
|
+
<span
|
|
95
60
|
:title="copied ? 'Copied!' : `Copy ${copyText}`"
|
|
96
61
|
class="ml-1 inline-flex items-center text-zinc-500 hover:text-zinc-300"
|
|
97
62
|
@click="onCopy"
|
|
98
63
|
>
|
|
99
64
|
<Check v-if="copied" class="h-3 w-3 text-emerald-400" />
|
|
100
65
|
<Copy v-else class="h-3 w-3" />
|
|
101
|
-
</
|
|
102
|
-
</
|
|
66
|
+
</span>
|
|
67
|
+
</button>
|
|
103
68
|
</template>
|
|
@@ -6,7 +6,6 @@ describe("normalizeCache", () => {
|
|
|
6
6
|
const { cache, missingFields } = normalizeCache({});
|
|
7
7
|
expect(cache).not.toBeNull();
|
|
8
8
|
expect(cache?.apps).toEqual([]);
|
|
9
|
-
expect(cache?.modules).toEqual([]);
|
|
10
9
|
expect(cache?.actions).toEqual([]);
|
|
11
10
|
expect(cache?.events).toEqual([]);
|
|
12
11
|
expect(cache?.actors).toEqual([]);
|
|
@@ -23,8 +22,7 @@ describe("normalizeCache", () => {
|
|
|
23
22
|
|
|
24
23
|
it("preserves arrays that ARE present", () => {
|
|
25
24
|
const input = {
|
|
26
|
-
apps: [{ name: "x",
|
|
27
|
-
modules: [],
|
|
25
|
+
apps: [{ name: "x", plugins: [] }],
|
|
28
26
|
actions: [],
|
|
29
27
|
events: [],
|
|
30
28
|
actors: [],
|
|
@@ -40,6 +38,8 @@ describe("normalizeCache", () => {
|
|
|
40
38
|
crons: [],
|
|
41
39
|
hooks: [],
|
|
42
40
|
plugins: [],
|
|
41
|
+
sinks: [],
|
|
42
|
+
bindings: [],
|
|
43
43
|
graph: { events: [] },
|
|
44
44
|
generatedAt: "2026-05-17T00:00:00Z",
|
|
45
45
|
};
|
|
@@ -52,13 +52,12 @@ describe("normalizeCache", () => {
|
|
|
52
52
|
it("reports the exact list of missing array fields", () => {
|
|
53
53
|
const { missingFields } = normalizeCache({
|
|
54
54
|
apps: [],
|
|
55
|
-
modules: [],
|
|
56
55
|
actions: [],
|
|
57
56
|
events: [],
|
|
58
57
|
actors: [],
|
|
59
58
|
projections: [],
|
|
60
59
|
queries: [],
|
|
61
|
-
// resolvers + workflows + graph missing
|
|
60
|
+
// resolvers + workflows + sinks + graph missing
|
|
62
61
|
routes: [],
|
|
63
62
|
externalCalls: [],
|
|
64
63
|
inboundWebhooks: [],
|
|
@@ -68,10 +67,12 @@ describe("normalizeCache", () => {
|
|
|
68
67
|
generatedAt: "2026-05-17T00:00:00Z",
|
|
69
68
|
});
|
|
70
69
|
expect([...missingFields].sort()).toEqual([
|
|
70
|
+
"bindings",
|
|
71
71
|
"graph",
|
|
72
72
|
"hooks",
|
|
73
73
|
"plugins",
|
|
74
74
|
"resolvers",
|
|
75
|
+
"sinks",
|
|
75
76
|
"workflows",
|
|
76
77
|
]);
|
|
77
78
|
});
|