orionfold-relay 0.28.0 → 0.29.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/dist/cli.js +5 -5
- package/package.json +1 -1
- package/src/app/api/instance/identity/route.ts +80 -0
- package/src/app/api/settings/glance/route.ts +166 -0
- package/src/app/api/telemetry/route.ts +1 -24
- package/src/app/globals.css +172 -6
- package/src/app/layout.tsx +6 -2
- package/src/app/page.tsx +4 -4
- package/src/components/apps/kit-view/kit-view.tsx +19 -0
- package/src/components/apps/last-run-card.tsx +11 -9
- package/src/components/apps/run-now-toast.ts +10 -7
- package/src/components/apps/starter-template-card.tsx +1 -1
- package/src/components/chat/chat-command-popover.tsx +3 -3
- package/src/components/settings/auth-status-dot.tsx +15 -1
- package/src/components/shell/app-bar.tsx +10 -7
- package/src/components/shell/app-shell.tsx +7 -1
- package/src/components/shell/bar-identity-cluster.tsx +48 -0
- package/src/components/shell/glance-rail.tsx +304 -0
- package/src/components/shell/rail-cell.tsx +8 -4
- package/src/components/shell/telemetry-rail.tsx +36 -12
- package/src/components/shell/use-instance-identity.ts +107 -0
- package/src/components/shell/use-settings-glance.ts +82 -0
- package/src/lib/apps/composition-detector.ts +1 -1
- package/src/lib/apps/registry.ts +1 -1
- package/src/lib/apps/view-kits/kits/workflow-hub.ts +12 -1
- package/src/lib/apps/view-kits/types.ts +8 -0
- package/src/lib/chat/command-tabs.ts +1 -1
- package/src/lib/chat/system-prompt.ts +3 -3
- package/src/lib/chat/tool-catalog.ts +9 -9
- package/src/lib/plugins/examples/echo-server/plugin.yaml +1 -1
- package/src/lib/plugins/examples/finance-pack/plugin.yaml +1 -1
- package/src/lib/plugins/examples/reading-radar/plugin.yaml +1 -1
- package/src/lib/plugins/registry.ts +1 -1
- package/src/lib/plugins/sdk/types.ts +1 -1
- package/src/lib/settings/runtime-setup.ts +32 -0
package/dist/cli.js
CHANGED
|
@@ -1186,7 +1186,7 @@ var CURRENT_PLUGIN_API_VERSION, CAPABILITY_VALUES, ORIGIN_VALUES, PrimitivesBund
|
|
|
1186
1186
|
var init_types = __esm({
|
|
1187
1187
|
"src/lib/plugins/sdk/types.ts"() {
|
|
1188
1188
|
"use strict";
|
|
1189
|
-
CURRENT_PLUGIN_API_VERSION = "0.
|
|
1189
|
+
CURRENT_PLUGIN_API_VERSION = "0.29";
|
|
1190
1190
|
CAPABILITY_VALUES = ["fs", "net", "child_process", "env"];
|
|
1191
1191
|
ORIGIN_VALUES = ["ainative-internal", "third-party"];
|
|
1192
1192
|
PrimitivesBundleManifestSchema = z.object({
|
|
@@ -3262,7 +3262,7 @@ function buildPrimitivesSummary(manifest) {
|
|
|
3262
3262
|
const tableCount = manifest.tables.length;
|
|
3263
3263
|
const scheduleCount = manifest.schedules.length;
|
|
3264
3264
|
if (profileCount > 0) {
|
|
3265
|
-
parts.push(pluralize(profileCount, "
|
|
3265
|
+
parts.push(pluralize(profileCount, "Agent", "agents"));
|
|
3266
3266
|
}
|
|
3267
3267
|
if (blueprintCount > 0) {
|
|
3268
3268
|
parts.push(pluralize(blueprintCount, "Blueprint", "blueprints"));
|
|
@@ -13010,7 +13010,7 @@ var init_registry6 = __esm({
|
|
|
13010
13010
|
init_registry5();
|
|
13011
13011
|
init_installer();
|
|
13012
13012
|
init_schedule_spec();
|
|
13013
|
-
SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.
|
|
13013
|
+
SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.28"]);
|
|
13014
13014
|
pluginCache = null;
|
|
13015
13015
|
lastLoadedPluginIds = /* @__PURE__ */ new Set();
|
|
13016
13016
|
PluginTableSchema = z16.object({
|
|
@@ -25941,8 +25941,8 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
25941
25941
|
import yaml12 from "js-yaml";
|
|
25942
25942
|
import semver from "semver";
|
|
25943
25943
|
function relayCoreVersion() {
|
|
25944
|
-
if (semver.valid("0.
|
|
25945
|
-
return "0.
|
|
25944
|
+
if (semver.valid("0.29.0")) {
|
|
25945
|
+
return "0.29.0";
|
|
25946
25946
|
}
|
|
25947
25947
|
try {
|
|
25948
25948
|
const root = getAppRoot(import.meta.dirname, 3);
|
package/package.json
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { valid as semverValid } from "semver";
|
|
3
|
+
import { relayCoreVersion } from "@/lib/packs/install";
|
|
4
|
+
import { getLicensedIdentity } from "@/lib/licensing/store";
|
|
5
|
+
import { getRuntimeSetupStates, pickActiveRuntime } from "@/lib/settings/runtime-setup";
|
|
6
|
+
import { resolvePreferredModel } from "@/lib/agents/runtime/model-preference";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GET /api/instance/identity
|
|
10
|
+
*
|
|
11
|
+
* The consolidated instance-identity read for the top-chrome bar cluster and
|
|
12
|
+
* the rail RUNTIME cell. One poll, one loading state, three fields:
|
|
13
|
+
*
|
|
14
|
+
* - version: the running relay-core version ("0.28.0"), or null.
|
|
15
|
+
* - activeModel: the concrete model the active runtime would run ("claude-opus-4-8"),
|
|
16
|
+
* or null when nothing resolves.
|
|
17
|
+
* - licenseTag: a discriminated union — { kind:"licensed", label } or { kind:"community" }.
|
|
18
|
+
*
|
|
19
|
+
* Two shadow-path rules (Engineering Principle #3 — data flows have shadow paths):
|
|
20
|
+
*
|
|
21
|
+
* 1. `version` is null, NEVER "0.0.0". `relayCoreVersion()` falls back to
|
|
22
|
+
* "0.0.0" when the build-time `__RELAY_CORE_VERSION__` global is missing or
|
|
23
|
+
* malformed (the Next 16 `defineServer` raw-string gotcha). We surface that
|
|
24
|
+
* fallback as `null` so the bar renders NOTHING rather than a wrong version.
|
|
25
|
+
* Absent > wrong.
|
|
26
|
+
* 2. `licenseTag` is a discriminated union, never a nullable string, so a
|
|
27
|
+
* missing name can never render as a dangling "Licensed to ". The store's
|
|
28
|
+
* `getLicensedIdentity()` already fails OPEN to community (null); the union
|
|
29
|
+
* makes that community fallback explicit at the type level.
|
|
30
|
+
*
|
|
31
|
+
* This is a live read of mutable server state (license files, runtime auth,
|
|
32
|
+
* model preference) — never cached.
|
|
33
|
+
*/
|
|
34
|
+
export const dynamic = "force-dynamic";
|
|
35
|
+
|
|
36
|
+
export type LicenseTag =
|
|
37
|
+
| { kind: "licensed"; label: string }
|
|
38
|
+
| { kind: "community" };
|
|
39
|
+
|
|
40
|
+
export interface InstanceIdentityResponse {
|
|
41
|
+
version: string | null;
|
|
42
|
+
activeModel: string | null;
|
|
43
|
+
licenseTag: LicenseTag;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function GET() {
|
|
47
|
+
try {
|
|
48
|
+
// Version: treat the "0.0.0" build-fallback as absent (rule 1).
|
|
49
|
+
const rawVersion = relayCoreVersion();
|
|
50
|
+
const version =
|
|
51
|
+
semverValid(rawVersion) && rawVersion !== "0.0.0" ? rawVersion : null;
|
|
52
|
+
|
|
53
|
+
// License tag: fails open to community (rule 2).
|
|
54
|
+
const label = getLicensedIdentity();
|
|
55
|
+
const licenseTag: LicenseTag = label
|
|
56
|
+
? { kind: "licensed", label }
|
|
57
|
+
: { kind: "community" };
|
|
58
|
+
|
|
59
|
+
// Active model: the concrete model the active runtime would run.
|
|
60
|
+
let activeModel: string | null = null;
|
|
61
|
+
try {
|
|
62
|
+
const states = await getRuntimeSetupStates();
|
|
63
|
+
const { runtimeId } = pickActiveRuntime(states);
|
|
64
|
+
activeModel = (await resolvePreferredModel(runtimeId)).modelId;
|
|
65
|
+
} catch {
|
|
66
|
+
// No runtime configured / preference read failed — leave null; the rail
|
|
67
|
+
// falls back to the runtimeLabel so the cell is never blank.
|
|
68
|
+
activeModel = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const body: InstanceIdentityResponse = { version, activeModel, licenseTag };
|
|
72
|
+
return NextResponse.json(body);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
75
|
+
// Zero silent failures — surface the fault to server logs, and let the
|
|
76
|
+
// client hook fall into its `status:"error"` branch (cluster renders nothing).
|
|
77
|
+
console.error("[instance/identity] read failed:", message);
|
|
78
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { count } from "drizzle-orm";
|
|
3
|
+
import { getLicensedIdentity } from "@/lib/licensing/store";
|
|
4
|
+
import {
|
|
5
|
+
getRuntimeSetupStates,
|
|
6
|
+
pickActiveRuntime,
|
|
7
|
+
} from "@/lib/settings/runtime-setup";
|
|
8
|
+
import { resolvePreferredModel } from "@/lib/agents/runtime/model-preference";
|
|
9
|
+
import { getBudgetGuardrailSnapshot } from "@/lib/settings/budget-guardrails";
|
|
10
|
+
import { getRoutingPreference } from "@/lib/settings/routing";
|
|
11
|
+
import { getActivePresets } from "@/lib/settings/permission-presets";
|
|
12
|
+
import { getAllowedPermissions } from "@/lib/settings/permissions";
|
|
13
|
+
import { getSetting } from "@/lib/settings/helpers";
|
|
14
|
+
import { SETTINGS_KEYS, type RoutingPreference } from "@/lib/constants/settings";
|
|
15
|
+
import { db } from "@/lib/db";
|
|
16
|
+
import { channelConfigs } from "@/lib/db/schema";
|
|
17
|
+
import type { LicenseTag } from "@/app/api/instance/identity/route";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* GET /api/settings/glance
|
|
21
|
+
*
|
|
22
|
+
* The consolidated settings read for the FEAT-14 settings-at-a-glance rail.
|
|
23
|
+
* One poll, one loading state, four grouped clusters (Runtime · Budget ·
|
|
24
|
+
* Permissions · Integrations) summarized as compact read-only fields. Aggregates
|
|
25
|
+
* the ~8 settings lib sources SERVER-SIDE (direct lib calls in one Promise.all,
|
|
26
|
+
* never an HTTP fan-out), mirroring /api/instance/identity's discipline.
|
|
27
|
+
*
|
|
28
|
+
* Shadow-path rules (Engineering Principle #3 — data flows have shadow paths):
|
|
29
|
+
* EVERY field is independently nullable, and each source is resolved in its own
|
|
30
|
+
* `.catch(() => null)` so one failing read (e.g. no runtime configured) NULLS
|
|
31
|
+
* only its own field — the rail still renders the chips that resolved. A total
|
|
32
|
+
* route failure returns 500 and the client hook collapses the rail to nothing
|
|
33
|
+
* (no crash, no half-rendered skeleton). Absent > wrong, everywhere.
|
|
34
|
+
*
|
|
35
|
+
* Live read of mutable server state (license files, runtime auth, budget policy,
|
|
36
|
+
* permission allow-list, channel rows) — never cached.
|
|
37
|
+
*/
|
|
38
|
+
export const dynamic = "force-dynamic";
|
|
39
|
+
|
|
40
|
+
export type { LicenseTag };
|
|
41
|
+
|
|
42
|
+
export interface SettingsGlanceResponse {
|
|
43
|
+
// Runtime cluster
|
|
44
|
+
activeRuntimeLabel: string | null;
|
|
45
|
+
activeModel: string | null;
|
|
46
|
+
routingPreference: RoutingPreference | null;
|
|
47
|
+
configuredRuntimeCount: number | null;
|
|
48
|
+
sdkTimeoutSeconds: number | null;
|
|
49
|
+
maxTurns: number | null;
|
|
50
|
+
// Budget cluster
|
|
51
|
+
licenseTag: LicenseTag;
|
|
52
|
+
budgetMonthlyCapUsd: number | null;
|
|
53
|
+
// Permissions cluster
|
|
54
|
+
activePreset: string | null; // most-permissive active preset id, or null
|
|
55
|
+
allowedPermissionCount: number | null;
|
|
56
|
+
// Integrations cluster
|
|
57
|
+
webSearchEnabled: boolean | null;
|
|
58
|
+
channelCount: number | null;
|
|
59
|
+
autoPromoteSkills: boolean | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The active preset id, choosing the MOST permissive when several are fully
|
|
63
|
+
// satisfied by the current allow-list (full-auto ⊃ git-safe ⊃ read-only), so
|
|
64
|
+
// the glance reports the effective posture rather than a subset preset.
|
|
65
|
+
function pickPreset(activePresets: string[]): string | null {
|
|
66
|
+
for (const id of ["full-auto", "git-safe", "read-only"]) {
|
|
67
|
+
if (activePresets.includes(id)) return id;
|
|
68
|
+
}
|
|
69
|
+
return activePresets[0] ?? null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function resolveRuntime(): Promise<{
|
|
73
|
+
label: string | null;
|
|
74
|
+
model: string | null;
|
|
75
|
+
configuredCount: number | null;
|
|
76
|
+
}> {
|
|
77
|
+
const states = await getRuntimeSetupStates();
|
|
78
|
+
const { runtimeId, runtimeLabel } = pickActiveRuntime(states);
|
|
79
|
+
// Count runtimes the user has actually set up (any state marked configured).
|
|
80
|
+
const configuredCount = Object.values(states).filter(
|
|
81
|
+
(s) => (s as { configured?: boolean }).configured,
|
|
82
|
+
).length;
|
|
83
|
+
let model: string | null = null;
|
|
84
|
+
try {
|
|
85
|
+
model = (await resolvePreferredModel(runtimeId)).modelId;
|
|
86
|
+
} catch {
|
|
87
|
+
model = null;
|
|
88
|
+
}
|
|
89
|
+
return { label: runtimeLabel, model, configuredCount };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Parse a numeric setting stored as a string; null/NaN → null (shadow path).
|
|
93
|
+
function numSetting(raw: string | null): number | null {
|
|
94
|
+
if (raw == null) return null;
|
|
95
|
+
const n = Number(raw);
|
|
96
|
+
return Number.isFinite(n) ? n : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function countChannels(): Promise<number> {
|
|
100
|
+
const [row] = await db.select({ value: count() }).from(channelConfigs);
|
|
101
|
+
return row?.value ?? 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function GET() {
|
|
105
|
+
try {
|
|
106
|
+
// Each source resolves independently; a single failing read nulls only its
|
|
107
|
+
// own field(s) — the rail renders whatever resolved.
|
|
108
|
+
const [
|
|
109
|
+
runtime,
|
|
110
|
+
label,
|
|
111
|
+
budget,
|
|
112
|
+
routing,
|
|
113
|
+
presets,
|
|
114
|
+
allowed,
|
|
115
|
+
exa,
|
|
116
|
+
channels,
|
|
117
|
+
sdkTimeout,
|
|
118
|
+
maxTurns,
|
|
119
|
+
autoPromote,
|
|
120
|
+
] = await Promise.all([
|
|
121
|
+
resolveRuntime().catch(() => ({
|
|
122
|
+
label: null,
|
|
123
|
+
model: null,
|
|
124
|
+
configuredCount: null,
|
|
125
|
+
})),
|
|
126
|
+
Promise.resolve(getLicensedIdentity()).catch(() => null),
|
|
127
|
+
getBudgetGuardrailSnapshot().catch(() => null),
|
|
128
|
+
getRoutingPreference().catch(() => null),
|
|
129
|
+
getActivePresets().catch(() => null),
|
|
130
|
+
getAllowedPermissions().catch(() => null),
|
|
131
|
+
getSetting(SETTINGS_KEYS.EXA_SEARCH_MCP_ENABLED).catch(() => null),
|
|
132
|
+
countChannels().catch(() => null),
|
|
133
|
+
getSetting(SETTINGS_KEYS.SDK_TIMEOUT_SECONDS).catch(() => null),
|
|
134
|
+
getSetting(SETTINGS_KEYS.MAX_TURNS).catch(() => null),
|
|
135
|
+
getSetting(SETTINGS_KEYS.AUTO_PROMOTE_SKILLS).catch(() => null),
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
const licenseTag: LicenseTag = label
|
|
139
|
+
? { kind: "licensed", label }
|
|
140
|
+
: { kind: "community" };
|
|
141
|
+
|
|
142
|
+
const body: SettingsGlanceResponse = {
|
|
143
|
+
activeRuntimeLabel: runtime.label,
|
|
144
|
+
activeModel: runtime.model,
|
|
145
|
+
routingPreference: routing,
|
|
146
|
+
configuredRuntimeCount: runtime.configuredCount,
|
|
147
|
+
sdkTimeoutSeconds: numSetting(sdkTimeout),
|
|
148
|
+
maxTurns: numSetting(maxTurns),
|
|
149
|
+
licenseTag,
|
|
150
|
+
budgetMonthlyCapUsd: budget?.policy.overall.monthlySpendCapUsd ?? null,
|
|
151
|
+
activePreset: presets ? pickPreset(presets) : null,
|
|
152
|
+
allowedPermissionCount: allowed?.length ?? null,
|
|
153
|
+
// exa is stored as the string "true"/"false"; null (unread) → null, not false.
|
|
154
|
+
webSearchEnabled: exa == null ? null : exa === "true",
|
|
155
|
+
channelCount: channels,
|
|
156
|
+
autoPromoteSkills: autoPromote == null ? null : autoPromote === "true",
|
|
157
|
+
};
|
|
158
|
+
return NextResponse.json(body);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
161
|
+
// Zero silent failures — surface to server logs, let the hook fall into
|
|
162
|
+
// status:"error" (the whole rail collapses to nothing).
|
|
163
|
+
console.error("[settings/glance] read failed:", message);
|
|
164
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -17,12 +17,7 @@ import {
|
|
|
17
17
|
getCompletionsByDay,
|
|
18
18
|
getFailuresByDay,
|
|
19
19
|
} from "@/lib/queries/chart-data";
|
|
20
|
-
import {
|
|
21
|
-
DEFAULT_AGENT_RUNTIME,
|
|
22
|
-
SUPPORTED_AGENT_RUNTIMES,
|
|
23
|
-
type AgentRuntimeId,
|
|
24
|
-
} from "@/lib/agents/runtime/catalog";
|
|
25
|
-
import type { RuntimeSetupState } from "@/lib/settings/runtime-setup";
|
|
20
|
+
import { pickActiveRuntime } from "@/lib/settings/runtime-setup";
|
|
26
21
|
import type { TelemetrySnapshot } from "@/components/shell/telemetry-types";
|
|
27
22
|
|
|
28
23
|
// Telemetry is a live read of mutable server state; never let a route or the
|
|
@@ -97,24 +92,6 @@ function getHostMetrics(): { cpuLoadPct: number | null; memUsedPct: number | nul
|
|
|
97
92
|
return { cpuLoadPct, memUsedPct };
|
|
98
93
|
}
|
|
99
94
|
|
|
100
|
-
// Pick the runtime to surface in the RUNTIME cell: the default (claude-code) if
|
|
101
|
-
// it is configured, otherwise the first configured runtime in catalog order, and
|
|
102
|
-
// failing that the default's label (so the cell shows "Claude Code · anthropic"
|
|
103
|
-
// with the understanding it is not yet set up, rather than an empty cell).
|
|
104
|
-
function pickActiveRuntime(
|
|
105
|
-
states: Record<AgentRuntimeId, RuntimeSetupState>,
|
|
106
|
-
): { runtimeLabel: string | null; providerId: string | null } {
|
|
107
|
-
const ordered: AgentRuntimeId[] = [
|
|
108
|
-
DEFAULT_AGENT_RUNTIME,
|
|
109
|
-
...SUPPORTED_AGENT_RUNTIMES.filter((id) => id !== DEFAULT_AGENT_RUNTIME),
|
|
110
|
-
];
|
|
111
|
-
const configured = ordered.find((id) => states[id]?.configured);
|
|
112
|
-
const chosen = configured ?? DEFAULT_AGENT_RUNTIME;
|
|
113
|
-
const state = states[chosen];
|
|
114
|
-
if (!state) return { runtimeLabel: null, providerId: null };
|
|
115
|
-
return { runtimeLabel: state.label, providerId: state.providerId };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
95
|
export async function GET() {
|
|
119
96
|
try {
|
|
120
97
|
const todayMidnight = new Date();
|
package/src/app/globals.css
CHANGED
|
@@ -103,6 +103,25 @@
|
|
|
103
103
|
--surface-3: oklch(0.96 0.006 250);
|
|
104
104
|
--surface-foreground: oklch(0.14 0.02 250);
|
|
105
105
|
|
|
106
|
+
/* Instrument palette (WS3, 2026-07-05) — the relay-website technique ported
|
|
107
|
+
into the app: a two-tier teal blueprint grid, translucent teal elevation
|
|
108
|
+
washes, and an accent glow, ALL derived from --primary so they track the
|
|
109
|
+
theme automatically (dark teal vs light teal, no .dark override needed).
|
|
110
|
+
These are ADDITIVE — surfaces stay neutral; the teal lives only in the
|
|
111
|
+
grid/glow/wash LAYERS painted over neutral surfaces (per the website). Light
|
|
112
|
+
washes run slightly hotter than dark since teal-on-white is subtle. */
|
|
113
|
+
--grid-line: color-mix(in oklch, var(--primary) 10%, transparent); /* fine 24px hairline */
|
|
114
|
+
--grid-major: color-mix(in oklch, var(--primary) 18%, transparent); /* major 120px line */
|
|
115
|
+
--glow-accent: color-mix(in oklch, var(--primary) 22%, transparent); /* ambient accent (light) */
|
|
116
|
+
--wash-1: color-mix(in oklch, var(--primary) 5%, transparent); /* faintest teal wash */
|
|
117
|
+
--wash-2: color-mix(in oklch, var(--primary) 8%, transparent);
|
|
118
|
+
--wash-3: color-mix(in oklch, var(--primary) 12%, transparent); /* strongest teal wash */
|
|
119
|
+
/* The translucent instrument rail: a tint of --surface-2 the grid reads
|
|
120
|
+
through, + a faint teal seam-glow on the rail's top edge. */
|
|
121
|
+
/* Rail is now 100% OPAQUE per operator — solid --surface-2, no grid through. */
|
|
122
|
+
--rail-surface: var(--surface-2);
|
|
123
|
+
--rail-glow: color-mix(in oklch, var(--primary) 12%, transparent);
|
|
124
|
+
|
|
106
125
|
/* Accent */
|
|
107
126
|
--accent: oklch(0.955 0.01 250);
|
|
108
127
|
--accent-foreground: oklch(0.25 0.03 250);
|
|
@@ -173,6 +192,34 @@
|
|
|
173
192
|
--space-10: 40px;
|
|
174
193
|
--space-12: 48px;
|
|
175
194
|
--space-16: 64px;
|
|
195
|
+
|
|
196
|
+
/* Top-chrome geometry — the two-tier app bar's real stacked height. The
|
|
197
|
+
telemetry rail's sticky `top` MUST equal this (rail slides UNDER the header
|
|
198
|
+
otherwise). Theme-agnostic, so declared once here, not in .dark. */
|
|
199
|
+
--chrome-tier1: 3.5rem; /* h-14 — tier-1 nav bar */
|
|
200
|
+
--chrome-tier2: 2.75rem; /* h-11 — tier-2 sub-nav */
|
|
201
|
+
--chrome-header: calc(var(--chrome-tier1) + var(--chrome-tier2)); /* 100px */
|
|
202
|
+
/* The telemetry rail's height (was a magic h-[88px]). The settings-glance
|
|
203
|
+
collapsed row parks sticky BELOW the rail, so its `top` is header + rail;
|
|
204
|
+
tokenizing keeps that offset honest if the rail height ever changes. We do
|
|
205
|
+
NOT fold these into --chrome-header — that token is the telemetry rail's own
|
|
206
|
+
`top` and folding rail height in would shove the rail under the header. */
|
|
207
|
+
/* Telemetry rail height. The rail is now auto-height (content + symmetric
|
|
208
|
+
py-2.5), so this MUST equal that rendered height or the glance rail (which
|
|
209
|
+
parks at header + rail) leaves a gap / overlaps. Expressed in rem so it
|
|
210
|
+
tracks the root font like --chrome-header: the rail's 3-line cell +
|
|
211
|
+
2×0.625rem padding renders ~4.95rem (≈69px at the operator's 14px root). */
|
|
212
|
+
--chrome-rail: 4.95rem;
|
|
213
|
+
--chrome-glance-top: calc(var(--chrome-header) + var(--chrome-rail)); /* glance sticky top */
|
|
214
|
+
|
|
215
|
+
/* Named z-scale — formalizes values previously scattered as magic numbers
|
|
216
|
+
(z-20/z-30/100/9999). Header always wins over rail; grid sits below all
|
|
217
|
+
chrome; overlays and toasts stay on top. */
|
|
218
|
+
--z-canvas-grid: 0;
|
|
219
|
+
--z-rail: 30;
|
|
220
|
+
--z-header: 40;
|
|
221
|
+
--z-overlay: 100;
|
|
222
|
+
--z-toast: 9999;
|
|
176
223
|
}
|
|
177
224
|
|
|
178
225
|
/* =============================================================
|
|
@@ -202,12 +249,33 @@
|
|
|
202
249
|
--muted: oklch(0.20 0.02 250); /* Derivation: matches secondary */
|
|
203
250
|
--muted-foreground: oklch(0.66 0.02 250); /* L0.66 on L0.14 bg ≈ 5.3:1 — clears WCAG AA 4.5:1 even un-diluted (was L0.58 ≈ 3.6:1, failed) */
|
|
204
251
|
|
|
205
|
-
/* Surfaces — 3-tier dark hierarchy
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
252
|
+
/* Surfaces — 3-tier dark hierarchy. Steps widened from 0.02L to ~0.035L:
|
|
253
|
+
near the dark end of oklch the eye reads a 0.02 lightness step as almost
|
|
254
|
+
flat (unlike light mode, where 0.025 near white reads clearly), so the
|
|
255
|
+
chrome tiers looked uniform. s-1 stays 0.18 (card/sidebar baseline — no
|
|
256
|
+
shift), s-2/s-3 recede further so the descending elevation is legible. */
|
|
257
|
+
--surface-1: oklch(0.185 0.02 250); /* frontmost — card/sidebar baseline */
|
|
258
|
+
--surface-2: oklch(0.15 0.02 250); /* steps back */
|
|
259
|
+
--surface-3: oklch(0.115 0.018 250); /* deepest — the instrument rail */
|
|
209
260
|
--surface-foreground: oklch(0.92 0.01 250);
|
|
210
261
|
|
|
262
|
+
/* Instrument palette (WS3) — dark. Derived from --primary (dark teal
|
|
263
|
+
oklch 0.78/.13/192). Grid alpha halved from 12/22% → 6/11% per operator
|
|
264
|
+
("50% darker"): the teal recedes toward the charcoal ground so the grid
|
|
265
|
+
reads subtler/dimmer. Washes track --primary; surfaces stay neutral. */
|
|
266
|
+
--grid-line: color-mix(in oklch, var(--primary) 6%, transparent); /* fine 24px hairline */
|
|
267
|
+
--grid-major: color-mix(in oklch, var(--primary) 11%, transparent); /* major 120px line */
|
|
268
|
+
--glow-accent: color-mix(in oklch, var(--primary) 26%, transparent); /* ambient accent (dark) */
|
|
269
|
+
--wash-1: color-mix(in oklch, var(--primary) 4%, transparent);
|
|
270
|
+
--wash-2: color-mix(in oklch, var(--primary) 6%, transparent);
|
|
271
|
+
--wash-3: color-mix(in oklch, var(--primary) 10%, transparent);
|
|
272
|
+
/* Dark rail: 100% OPAQUE per operator, on --surface-1 (L0.185) — a solid
|
|
273
|
+
lifted chrome band clearly above the canvas --background (0.14). --surface-1
|
|
274
|
+
(not --surface-2, which is only 0.01L above the bg) keeps the band distinct;
|
|
275
|
+
opaque means no grid shows through. */
|
|
276
|
+
--rail-surface: var(--surface-1);
|
|
277
|
+
--rail-glow: color-mix(in oklch, var(--primary) 14%, transparent);
|
|
278
|
+
|
|
211
279
|
/* Accent */
|
|
212
280
|
--accent: oklch(0.20 0.02 250);
|
|
213
281
|
--accent-foreground: oklch(0.92 0.01 250);
|
|
@@ -537,7 +605,7 @@
|
|
|
537
605
|
.of-boot {
|
|
538
606
|
position: fixed;
|
|
539
607
|
inset: 0;
|
|
540
|
-
z-index:
|
|
608
|
+
z-index: var(--z-overlay);
|
|
541
609
|
display: flex;
|
|
542
610
|
flex-direction: column;
|
|
543
611
|
align-items: center;
|
|
@@ -562,6 +630,104 @@
|
|
|
562
630
|
overflow: hidden;
|
|
563
631
|
}
|
|
564
632
|
|
|
633
|
+
/* =============================================================
|
|
634
|
+
BLUEPRINT GRID (FEAT-15 → instrument palette WS3, 2026-07-05)
|
|
635
|
+
Two grids, both FULL-BLEED (no radial mask):
|
|
636
|
+
|
|
637
|
+
• CANVAS (#main-content): the two-tier TEAL grid (fine 24px
|
|
638
|
+
--grid-line + major 120px --grid-major), covering the canvas
|
|
639
|
+
EVENLY edge to edge.
|
|
640
|
+
• RAIL (.rail-instrument): a UNIFORM single-tier 24px grid.
|
|
641
|
+
|
|
642
|
+
NO radial edge-mask on either. The mask faded the grid before the
|
|
643
|
+
centered content edge, leaving a dark ungridded band that read as
|
|
644
|
+
a ~15px "border" framing the container (operator). A uniform grid
|
|
645
|
+
has no fade zone, so no perceived frame. `.rail-instrument` also
|
|
646
|
+
owns position:sticky (avoids the [aria-label] specificity trap
|
|
647
|
+
that clobbered it). Both derive from --primary → light/dark track
|
|
648
|
+
automatically. Painted on ::before at --z-canvas-grid (0).
|
|
649
|
+
============================================================= */
|
|
650
|
+
#main-content {
|
|
651
|
+
position: relative;
|
|
652
|
+
}
|
|
653
|
+
#main-content::before {
|
|
654
|
+
content: "";
|
|
655
|
+
position: absolute;
|
|
656
|
+
inset: 0;
|
|
657
|
+
z-index: var(--z-canvas-grid);
|
|
658
|
+
pointer-events: none;
|
|
659
|
+
/* Two tiers: fine 24px cells + major 120px cells. */
|
|
660
|
+
background-image:
|
|
661
|
+
repeating-linear-gradient(to right, var(--grid-line) 0 1px, transparent 1px 24px),
|
|
662
|
+
repeating-linear-gradient(to bottom, var(--grid-line) 0 1px, transparent 1px 24px),
|
|
663
|
+
repeating-linear-gradient(to right, var(--grid-major) 0 1px, transparent 1px 120px),
|
|
664
|
+
repeating-linear-gradient(to bottom, var(--grid-major) 0 1px, transparent 1px 120px);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/* Translucent instrument rail (WS2) — a tint of --surface-2 the grid reads
|
|
668
|
+
through, so the sparkline "graphs" pop while cell values stay legible + a
|
|
669
|
+
teal seam-glow (inset top) marks the chrome↔canvas seam. `.rail-instrument`
|
|
670
|
+
OWNS position:sticky here (specificity 0,1,0 on the class) instead of the
|
|
671
|
+
telemetry rail joining the grid's `[aria-label]` position:relative rule
|
|
672
|
+
(0,1,1) — that attribute selector out-specifies Tailwind's `.sticky` and
|
|
673
|
+
silently demoted the rail to relative, so it scrolled away. */
|
|
674
|
+
.rail-instrument {
|
|
675
|
+
position: sticky;
|
|
676
|
+
background: var(--rail-surface);
|
|
677
|
+
box-shadow: inset 0 1px 0 0 var(--rail-glow);
|
|
678
|
+
}
|
|
679
|
+
/* Telemetry-cell vertical dividers: --border (L0.26 dark) is too low-contrast
|
|
680
|
+
over the lifted rail band, so bump the rail's own cell dividers to
|
|
681
|
+
--border-strong (L0.32). Scoped to the rail (the [aria-label] attribute
|
|
682
|
+
selector out-specifies Tailwind's `.border-border`, so no source-order
|
|
683
|
+
reliance) — no other border-border consumer shifts. */
|
|
684
|
+
[aria-label="Telemetry"] > * {
|
|
685
|
+
border-right-color: var(--border-strong);
|
|
686
|
+
}
|
|
687
|
+
/* The rail is now 100% OPAQUE (--rail-surface = solid surface), so it carries NO
|
|
688
|
+
grid: an opaque background does not hide a ::before grid (the pseudo paints
|
|
689
|
+
ABOVE the background), so the grid ::before was removed rather than relying on
|
|
690
|
+
opacity. The rail is a clean solid band; the canvas keeps its own grid. */
|
|
691
|
+
|
|
692
|
+
/* =============================================================
|
|
693
|
+
SETTINGS-AT-A-GLANCE RAIL (FEAT-14, WS4 · 2026-07-05)
|
|
694
|
+
A two-level progressive-disclosure rail below the telemetry
|
|
695
|
+
rail. It reads as ONE coherent chrome surface (the same
|
|
696
|
+
translucent instrument family as the telemetry rail), NOT a
|
|
697
|
+
stack of detached cards. Depth is carried by teal WASHES and
|
|
698
|
+
HAIRLINES, never by lighter/darker solid bands (that inverted
|
|
699
|
+
the hierarchy and made pills read as inconsistent dark strips):
|
|
700
|
+
• row collapsed chip row — translucent chrome + seam-glow
|
|
701
|
+
• panel expanded ground — same chrome family as the row
|
|
702
|
+
• tile group cluster — faint teal wash + hairline, no hard card
|
|
703
|
+
• pill key/value line — borderless, a hairline rule between
|
|
704
|
+
rows; the value sits on the tile, not on a solid band
|
|
705
|
+
Collapsed row is sticky under the telemetry rail (top =
|
|
706
|
+
--chrome-glance-top); the expanded panel pushes content (no jank).
|
|
707
|
+
============================================================= */
|
|
708
|
+
.glance-row {
|
|
709
|
+
/* Translucent chrome over the grid + a teal seam-glow on the TOP
|
|
710
|
+
edge marking the telemetry-rail↔glance seam. */
|
|
711
|
+
background: var(--rail-surface);
|
|
712
|
+
box-shadow: inset 0 1px 0 0 var(--rail-glow);
|
|
713
|
+
}
|
|
714
|
+
.glance-panel {
|
|
715
|
+
/* Expanded ground — the SAME translucent chrome family as the row,
|
|
716
|
+
so panel + row read as one surface (not a lighter/darker band). */
|
|
717
|
+
background: var(--rail-surface);
|
|
718
|
+
}
|
|
719
|
+
/* Compact cell — the SAME grammar as the telemetry RailCell (mono-uppercase
|
|
720
|
+
label over a value, left-aligned, a right-hairline vertical divider between
|
|
721
|
+
cells). Used for BOTH the collapsed summary chips and the expanded panel
|
|
722
|
+
cells, so the glance rail speaks one visual language with the telemetry rail
|
|
723
|
+
above it. The last cell in a row drops its divider. */
|
|
724
|
+
.glance-cell {
|
|
725
|
+
border-right: 1px solid var(--grid-line);
|
|
726
|
+
}
|
|
727
|
+
.glance-cell:last-child {
|
|
728
|
+
border-right: none;
|
|
729
|
+
}
|
|
730
|
+
|
|
565
731
|
/* =============================================================
|
|
566
732
|
BASE STYLES
|
|
567
733
|
============================================================= */
|
|
@@ -587,7 +753,7 @@ body {
|
|
|
587
753
|
width: 1px;
|
|
588
754
|
height: 1px;
|
|
589
755
|
overflow: hidden;
|
|
590
|
-
z-index:
|
|
756
|
+
z-index: var(--z-toast);
|
|
591
757
|
}
|
|
592
758
|
.skip-nav:focus {
|
|
593
759
|
position: fixed;
|
package/src/app/layout.tsx
CHANGED
|
@@ -65,8 +65,12 @@ const CRITICAL_THEME_CSS = `
|
|
|
65
65
|
color-scheme: dark;
|
|
66
66
|
--background: oklch(0.14 0.02 250);
|
|
67
67
|
--foreground: oklch(0.92 0.01 250);
|
|
68
|
-
--surface
|
|
69
|
-
|
|
68
|
+
/* KEEP IN SYNC with globals.css .dark --surface-* (widened tier steps,
|
|
69
|
+
2026-07-04). This inline critical-CSS wins via html.dark specificity
|
|
70
|
+
during the anti-FOUC window; a stale copy here silently overrides the
|
|
71
|
+
real tokens. */
|
|
72
|
+
--surface-1: oklch(0.185 0.02 250);
|
|
73
|
+
--surface-2: oklch(0.15 0.02 250);
|
|
70
74
|
--border: oklch(0.26 0.015 250);
|
|
71
75
|
}
|
|
72
76
|
/* Root rem base. Fixed 14px left the whole rem-based design tiny on high-res
|
package/src/app/page.tsx
CHANGED
|
@@ -81,8 +81,8 @@ export default async function HomePage() {
|
|
|
81
81
|
// the welcome path so we don't pay the read on every dashboard hit.
|
|
82
82
|
const starters = listStarters();
|
|
83
83
|
return (
|
|
84
|
-
<div className="bg-background min-h-screen
|
|
85
|
-
<div className="surface-page-shell min-h-
|
|
84
|
+
<div className="bg-background min-h-screen">
|
|
85
|
+
<div className="surface-page-shell min-h-screen p-5 sm:p-6 lg:p-7 space-y-6">
|
|
86
86
|
<WelcomeLanding starters={starters} />
|
|
87
87
|
</div>
|
|
88
88
|
</div>
|
|
@@ -176,8 +176,8 @@ export default async function HomePage() {
|
|
|
176
176
|
);
|
|
177
177
|
|
|
178
178
|
return (
|
|
179
|
-
<div className="bg-background min-h-screen
|
|
180
|
-
<div className="surface-page-shell min-h-
|
|
179
|
+
<div className="bg-background min-h-screen">
|
|
180
|
+
<div className="surface-page-shell min-h-screen p-5 sm:p-6 lg:p-7">
|
|
181
181
|
<Greeting
|
|
182
182
|
runningCount={runningResult.count}
|
|
183
183
|
awaitingCount={awaitingResult.count}
|
|
@@ -31,6 +31,25 @@ export function KitView({ model }: KitViewProps) {
|
|
|
31
31
|
{model.secondaryLead}
|
|
32
32
|
</p>
|
|
33
33
|
)}
|
|
34
|
+
{model.secondarySteps && model.secondarySteps.length > 0 && (
|
|
35
|
+
<ol className="flex flex-wrap items-center gap-x-2 gap-y-1.5">
|
|
36
|
+
{model.secondarySteps.map((step, i) => (
|
|
37
|
+
<li key={step.n} className="flex items-center gap-2">
|
|
38
|
+
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-medium text-primary">
|
|
39
|
+
{step.n}
|
|
40
|
+
</span>
|
|
41
|
+
<span className="text-sm text-muted-foreground">
|
|
42
|
+
{step.text}
|
|
43
|
+
</span>
|
|
44
|
+
{i < model.secondarySteps!.length - 1 && (
|
|
45
|
+
<span aria-hidden className="text-muted-foreground/40">
|
|
46
|
+
→
|
|
47
|
+
</span>
|
|
48
|
+
)}
|
|
49
|
+
</li>
|
|
50
|
+
))}
|
|
51
|
+
</ol>
|
|
52
|
+
)}
|
|
34
53
|
<SecondarySlotView slots={model.secondary} />
|
|
35
54
|
</div>
|
|
36
55
|
)}
|
|
@@ -166,16 +166,18 @@ export function RunnableBlueprintCard({
|
|
|
166
166
|
variables={card.variables}
|
|
167
167
|
label="Run"
|
|
168
168
|
/>
|
|
169
|
-
{/* FEAT-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
{/* CF-FEAT-5: name both verbs on EVERY card, not just the primary,
|
|
170
|
+
so no card leaves the two buttons unexplained. The "Start here"
|
|
171
|
+
card carries the fuller sentence (it may also mention the
|
|
172
|
+
variable prompt); the rest carry a compact one-liner so the grid
|
|
173
|
+
stays scannable (progressive disclosure). */}
|
|
174
|
+
<p className="text-xs text-muted-foreground">
|
|
175
|
+
{card.isPrimary
|
|
176
|
+
? card.variables.length > 0
|
|
175
177
|
? "Run asks a few questions, then starts a workflow you can watch. Create workflow saves a draft to run later."
|
|
176
|
-
: "Run starts a workflow you can watch. Create workflow saves a draft to run later."
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
: "Run starts a workflow you can watch. Create workflow saves a draft to run later."
|
|
179
|
+
: "Run starts it now. Create workflow saves a draft."}
|
|
180
|
+
</p>
|
|
179
181
|
</div>
|
|
180
182
|
)}
|
|
181
183
|
</CardContent>
|