Skip to content

Commit 92d7bde

Browse files
committed
better typescript machinery to check for extra keys, add a new skill for creating waveenv narrowing
1 parent 4ad7bad commit 92d7bde

4 files changed

Lines changed: 157 additions & 6 deletions

File tree

.kilocode/skills/waveenv/SKILL.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
name: waveenv
3+
description: Guide for creating WaveEnv narrowings in Wave Terminal. Use when writing a named subset type of WaveEnv for a component tree, documenting environmental dependencies, or enabling mock environments for preview/test server usage.
4+
---
5+
6+
# WaveEnv Narrowing Skill
7+
8+
## Purpose
9+
10+
A WaveEnv narrowing creates a _named subset type_ of `WaveEnv` that:
11+
12+
1. Documents exactly which parts of the environment a component tree actually uses.
13+
2. Forms a type contract so callers and tests know what to provide.
14+
3. Enables mocking in the preview/test server — you only need to implement what's listed.
15+
16+
## When To Create One
17+
18+
Create a narrowing whenever you are writing a component (or group of components) that you want to test in the preview server, or when you want to make the environmental dependencies of a component tree explicit.
19+
20+
## Core Principle: Only Include What You Use
21+
22+
**Only list the fields, methods, atoms, and keys that the component tree actually accesses.** If you don't call `wos`, don't include `wos`. If you only call one RPC command, only list that one command. The narrowing is a precise dependency declaration — not a copy of `WaveEnv`.
23+
24+
## File Location
25+
26+
- **Separate file** (preferred for shared/complex envs): name it `<feature>env.ts` next to the component, e.g. [`frontend/app/block/blockenv.ts`](frontend/app/block/blockenv.ts).
27+
- **Inline** (acceptable for small, single-file components): export the type directly from the component file, e.g. `WidgetsEnv` in [`frontend/app/workspace/widgets.tsx`](frontend/app/workspace/widgets.tsx:23).
28+
29+
## Imports Required
30+
31+
```ts
32+
import {
33+
BlockMetaKeyAtomFnType, // only if you use getBlockMetaKeyAtom
34+
ConnConfigKeyAtomFnType, // only if you use getConnConfigKeyAtom
35+
SettingsKeyAtomFnType, // only if you use getSettingsKeyAtom
36+
WaveEnv,
37+
WaveEnvSubset,
38+
} from "@/app/waveenv/waveenv";
39+
```
40+
41+
## The Shape
42+
43+
```ts
44+
export type MyEnv = WaveEnvSubset<{
45+
// --- Simple WaveEnv properties ---
46+
// Copy the type verbatim from WaveEnv with WaveEnv["key"] syntax.
47+
isDev: WaveEnv["isDev"];
48+
createBlock: WaveEnv["createBlock"];
49+
showContextMenu: WaveEnv["showContextMenu"];
50+
platform: WaveEnv["platform"];
51+
52+
// --- electron: list only the methods you call ---
53+
electron: {
54+
openExternal: WaveEnv["electron"]["openExternal"];
55+
};
56+
57+
// --- rpc: list only the commands you call ---
58+
rpc: {
59+
ActivityCommand: WaveEnv["rpc"]["ActivityCommand"];
60+
ConnEnsureCommand: WaveEnv["rpc"]["ConnEnsureCommand"];
61+
};
62+
63+
// --- atoms: list only the atoms you read ---
64+
atoms: {
65+
modalOpen: WaveEnv["atoms"]["modalOpen"];
66+
fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"];
67+
};
68+
69+
// --- wos: always take the whole thing, no sub-typing needed ---
70+
wos: WaveEnv["wos"];
71+
72+
// --- key-parameterized atom factories: enumerate the keys you use ---
73+
getSettingsKeyAtom: SettingsKeyAtomFnType<
74+
| "app:focusfollowscursor"
75+
| "window:magnifiedblockopacity"
76+
>;
77+
getBlockMetaKeyAtom: BlockMetaKeyAtomFnType<
78+
| "view"
79+
| "frame:title"
80+
| "connection"
81+
>;
82+
getConnConfigKeyAtom: ConnConfigKeyAtomFnType<"conn:wshenabled">;
83+
84+
// --- other atom helpers: copy verbatim ---
85+
getConnStatusAtom: WaveEnv["getConnStatusAtom"];
86+
getLocalHostDisplayNameAtom: WaveEnv["getLocalHostDisplayNameAtom"];
87+
}>;
88+
```
89+
90+
### Rules for Each Section
91+
92+
| Section | Pattern | Notes |
93+
|---|---|---|
94+
| `electron` | `electron: { method: WaveEnv["electron"]["method"]; }` | List every method called; omit the rest. |
95+
| `rpc` | `rpc: { Cmd: WaveEnv["rpc"]["Cmd"]; }` | List every RPC command called; omit the rest. |
96+
| `atoms` | `atoms: { atom: WaveEnv["atoms"]["atom"]; }` | List every atom read; omit the rest. |
97+
| `wos` | `wos: WaveEnv["wos"]` | Take the whole `wos` object (no sub-typing needed), but **only add it if `wos` is actually used**. |
98+
| `getSettingsKeyAtom` | `SettingsKeyAtomFnType<"key1" \| "key2">` | Union all settings keys accessed. |
99+
| `getBlockMetaKeyAtom` | `BlockMetaKeyAtomFnType<"key1" \| "key2">` | Union all block meta keys accessed. |
100+
| `getConnConfigKeyAtom` | `ConnConfigKeyAtomFnType<"key1">` | Union all conn config keys accessed. |
101+
| All other `WaveEnv` fields | `WaveEnv["fieldName"]` | Copy type verbatim. |
102+
103+
## Using the Narrowed Type in Components
104+
105+
```ts
106+
import { useWaveEnv } from "@/app/waveenv/waveenv";
107+
import { MyEnv } from "./myenv";
108+
109+
const MyComponent = memo(() => {
110+
const env = useWaveEnv<MyEnv>();
111+
// TypeScript now enforces you only access what's in MyEnv.
112+
const val = useAtomValue(env.getSettingsKeyAtom("app:focusfollowscursor"));
113+
...
114+
});
115+
```
116+
117+
The generic parameter on `useWaveEnv<MyEnv>()` casts the context to your narrowed type. The real production `WaveEnv` satisfies every narrowing; mock envs only need to implement the listed subset.
118+
119+
## Real Examples
120+
121+
- [`BlockEnv`](frontend/app/block/blockenv.ts:12) — complex narrowing with all section types, in a separate file.
122+
- [`WidgetsEnv`](frontend/app/workspace/widgets.tsx:23) — smaller narrowing defined inline in the component file.

frontend/app/block/blockenv.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
// Copyright 2026, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { BlockMetaKeyAtomFnType, ConnConfigKeyAtomFnType, SettingsKeyAtomFnType, WaveEnv } from "@/app/waveenv/waveenv";
4+
import {
5+
BlockMetaKeyAtomFnType,
6+
ConnConfigKeyAtomFnType,
7+
SettingsKeyAtomFnType,
8+
WaveEnv,
9+
WaveEnvSubset,
10+
} from "@/app/waveenv/waveenv";
511

6-
export type BlockEnv = {
12+
export type BlockEnv = WaveEnvSubset<{
713
getSettingsKeyAtom: SettingsKeyAtomFnType<
814
| "app:focusfollowscursor"
915
| "app:showoverlayblocknums"
@@ -39,4 +45,4 @@ export type BlockEnv = {
3945
| "frame:title"
4046
| "frame:icon"
4147
>;
42-
};
48+
}>;

frontend/app/waveenv/waveenv.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,29 @@ export type SettingsKeyAtomFnType<Keys extends keyof SettingsType = keyof Settin
2020
key: T
2121
) => Atom<SettingsType[T]>;
2222

23+
type OmitNever<T> = {
24+
[K in keyof T as [T[K]] extends [never] ? never : K]: T[K];
25+
};
26+
27+
type Subset<T, U> = OmitNever<{
28+
[K in keyof T]: K extends keyof U ? T[K] : never;
29+
}>;
30+
31+
type ComplexWaveEnvKeys = {
32+
rpc: WaveEnv["rpc"];
33+
electron: WaveEnv["electron"];
34+
atoms: WaveEnv["atoms"];
35+
wos: WaveEnv["wos"];
36+
};
37+
38+
export type WaveEnvSubset<T> = OmitNever<{
39+
[K in keyof T]: K extends keyof ComplexWaveEnvKeys
40+
? Subset<T[K], ComplexWaveEnvKeys[K]>
41+
: K extends keyof WaveEnv
42+
? T[K]
43+
: never;
44+
}>;
45+
2346
// default implementation for production is in ./waveenvimpl.ts
2447
export type WaveEnv = {
2548
electron: ElectronApi;

frontend/app/workspace/widgets.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { Tooltip } from "@/app/element/tooltip";
55
import { TabRpcClient } from "@/app/store/wshrpcutil";
6-
import { useWaveEnv, WaveEnv } from "@/app/waveenv/waveenv";
6+
import { useWaveEnv, WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv";
77
import { shouldIncludeWidgetForWorkspace } from "@/app/workspace/widgetfilter";
88
import { modalsModel } from "@/store/modalmodel";
99
import { fireAndForget, isBlank, makeIconClass } from "@/util/util";
@@ -20,7 +20,7 @@ import clsx from "clsx";
2020
import { useAtomValue } from "jotai";
2121
import { memo, useCallback, useEffect, useRef, useState } from "react";
2222

23-
export type WidgetsEnv = {
23+
export type WidgetsEnv = WaveEnvSubset<{
2424
isDev: WaveEnv["isDev"];
2525
electron: {
2626
openBuilder: WaveEnv["electron"]["openBuilder"];
@@ -35,7 +35,7 @@ export type WidgetsEnv = {
3535
};
3636
createBlock: WaveEnv["createBlock"];
3737
showContextMenu: WaveEnv["showContextMenu"];
38-
};
38+
}>;
3939

4040
function sortByDisplayOrder(wmap: { [key: string]: WidgetConfigType }): WidgetConfigType[] {
4141
if (wmap == null) {

0 commit comments

Comments
 (0)