Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion backend/tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def test_settings_payload_shape_matches_generated_validator_shape(manager):
def test_settings_payload_reflects_real_manager_defaults(manager):
payload = settings_payload(manager)
assert payload["theme"] == "dark"
assert payload["showTokenCounter"] is True
# R8a: the token counter overlay is off by default - opt-in, not opt-out.
assert payload["showTokenCounter"] is False
assert payload["enableSystemPrompt"] is True
assert payload["githubTokenConfigured"] is False
assert set(payload["notificationPreferences"]) == set(SettingsManager.NOTIFICATION_TYPES)
Expand Down Expand Up @@ -109,6 +110,13 @@ def test_set_show_token_counter_intent(manager):
bus = SessionBus("settings-token-counter-test")
register_settings(bus, manager)

# Default is now False - assert the intent actually flips state (True is
# the meaningful direction to prove; False-on-False would be a no-op
# that could pass even if the setter never ran at all).
assert manager.get_show_token_counter() is False
asyncio.run(bus.dispatch_intent("app-settings", "setShowTokenCounter", [True]))
assert manager.get_show_token_counter() is True

asyncio.run(bus.dispatch_intent("app-settings", "setShowTokenCounter", [False]))
assert manager.get_show_token_counter() is False

Expand Down
7 changes: 4 additions & 3 deletions graphlink_licensing.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def _load_state(self):
if 'theme' not in state:
state['theme'] = 'dark'
if 'show_token_counter' not in state:
state['show_token_counter'] = True
state['show_token_counter'] = False
state_changed = False
if 'ollama_chat_model' not in state:
state['ollama_chat_model'] = ''
Expand Down Expand Up @@ -207,7 +207,7 @@ def _create_initial_state(self):
state = {
"schema_version": self.CURRENT_SCHEMA_VERSION,
"theme": "dark",
"show_token_counter": True,
"show_token_counter": False,
"ollama_chat_model": "",
"ollama_title_model": "",
"ollama_chart_model": "",
Expand Down Expand Up @@ -346,7 +346,8 @@ def set_theme(self, theme_name):
self._save_state()

def get_show_token_counter(self):
return self.state.get("show_token_counter", True)
# R8a: off by default - the overlay is opt-in now, not opt-out.
return self.state.get("show_token_counter", False)

def set_show_token_counter(self, show: bool):
self.state['show_token_counter'] = show
Expand Down
9 changes: 5 additions & 4 deletions web_ui/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,11 @@ function GlobalShortcuts({ store }: { store: SceneStore }) {
function App() {
const [status, setStatus] = useState<ConnectionStatus>("closed");
const [system, setSystem] = useState<SystemState>({});
// showTokenCounter defaults true (matches AppSettingsStatePayload's
// default and the legacy AppearanceSettingsWidget's own initial state)
// until the real snapshot arrives, so the overlay doesn't flash hidden.
const [settingsVisibility, setSettingsVisibility] = useState<SettingsVisibilityState>({ showTokenCounter: true });
// showTokenCounter defaults false (R8a: off by default, matching
// SettingsManager.get_show_token_counter's own default) until the real
// snapshot arrives, so the overlay doesn't flash visible for a user who
// has it off.
const [settingsVisibility, setSettingsVisibility] = useState<SettingsVisibilityState>({ showTokenCounter: false });

const transport = useMemo(() => new WsTransport(defaultWsUrl()), []);
const sceneStore = useMemo(() => new SceneStore(transport), [transport]);
Expand Down
51 changes: 50 additions & 1 deletion web_ui/src/app/canvas/SceneCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,23 @@ export function makeDebouncedViewportReport(
};
}

// R8a: reads a real design-token value at render time rather than
// hardcoding a hex literal - needed anywhere a color has to be a plain JS
// string (an SVG-attribute-producing prop like MiniMap's nodeColor/
// nodeStrokeColor), not a CSS declaration value or a style={{}} block, so
// it falls outside what the no-raw-colors lint gate can enforce for us.
function useCssVar(name: string, fallback: string): string {
// A lazy initializer, not an effect: the token's value is static for the
// component's lifetime (no live theme-switching exists yet), so reading
// it once during the initial render avoids both a spurious extra render
// AND a one-frame flash of `fallback` before the real value lands.
const [value] = useState(() => {
const computed = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
return computed || fallback;
});
return value;
}

function CanvasInner({ store }: { store: SceneStore }) {
const scene = useSyncExternalStore(store.subscribe, store.getScene);
const grid = useSyncExternalStore(store.subscribe, store.getGrid);
Expand Down Expand Up @@ -996,6 +1013,31 @@ function CanvasInner({ store }: { store: SceneStore }) {

const edges = useMemo(() => toFlowEdges(scene, hoveredEdgeId), [scene, hoveredEdgeId]);

// R8a: the minimap used to render every node as React Flow's own default
// plain rectangle (no nodeColor/nodeStrokeColor was ever passed), which
// reads as flat, undifferentiated "white boxes" against this app's dark
// theme. note/frame/container are the only kinds with a real, user-
// assigned color (the same palette GroupColorPicker writes) - reflect it
// here so a colored group/note actually stands out on the minimap the
// way it does on the canvas. Every other kind (and any uncolored group/
// note) gets one deliberate, visible neutral instead of RF's default;
// the currently selected node gets the brightest tone so selection state
// reads on the minimap too. Colors are read from the real design tokens
// (not hardcoded hex) so this stays theme-driven.
const minimapNodeColor = useCssVar("--gl-surface-handle-hover", "#6A6A6A");
const minimapStrokeColor = useCssVar("--gl-surface-border-strong", "#505050");
const minimapSelectedColor = useCssVar("--gl-surface-text-bright", "#FFFFFF");
const getMinimapNodeColor = useCallback(
(node: SceneFlowNode) => {
if (node.selected) return minimapSelectedColor;
if ((node.type === "note" || node.type === "frame" || node.type === "container") && node.data.color) {
return node.data.color;
}
return minimapNodeColor;
},
[minimapNodeColor, minimapSelectedColor],
);

const onNodesChange = useCallback(
(changes: NodeChange<SceneFlowNode>[]) => {
// Synthetic member-position changes generated below (via
Expand Down Expand Up @@ -1195,7 +1237,14 @@ function CanvasInner({ store }: { store: SceneStore }) {
color={grid.gridColor}
style={{ opacity: grid.gridOpacityPercent / 100 }}
/>
<MiniMap pannable zoomable className="scene-minimap" />
<MiniMap
pannable
zoomable
className="scene-minimap"
nodeColor={getMinimapNodeColor}
nodeStrokeColor={minimapStrokeColor}
nodeStrokeWidth={2}
/>
{/* R7.5b-3: smart-guide lines. Legacy's guides were QGraphicsLineItems
in the same unified scene as the nodes, panning/zooming for free -
ViewportPortal is the direct React Flow analog (children render
Expand Down
138 changes: 29 additions & 109 deletions web_ui/src/app/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,26 @@
* Everything that floats above the canvas must take a value from here. Numbers
* are spaced so a tier can be inserted without renumbering its neighbours. */
:root {
--gl-z-canvas-chrome: 5; /* pins/token counter drawn over the canvas */
--gl-z-canvas-chrome: 5; /* persistent canvas HUD chrome (token counter) */
--gl-z-app-layer: 20; /* app-bar popovers, plugin picker, pin overlay */
--gl-z-search: 22; /* search overlay, above the other app layers */
--gl-z-notification: 30; /* banner must beat app layers, not dialogs */
--gl-z-scrim: 40; /* modal scrim + the dialog it dims for */
--gl-z-node-menu: 60; /* PORTALED node context menus (NodeMenu.tsx) */
--gl-z-approval: 80; /* blocking code-execution prompt - outranks all */

/* R8a: ONE canonical edge inset for every piece of chrome anchored to a
corner/edge of the canvas (pins, search, view/plugin popovers, token
counter, minimap). Before this, the SPA mixed 8px (pins/popovers/
search), 16px (token counter/notification), and React Flow's own
baked-in 15px (the minimap's default Panel margin, entirely outside
this app's own spacing system) - three near-identical values with no
stated relationship, which is exactly the kind of ad-hoc drift the
z-index scale above already fixed for stacking order. The composer
island's own bottom offset stays a distinct, separately-justified
value (verified against the token counter's real measured footprint),
not this token - it is not a plain edge inset. */
--gl-chrome-inset: 16px;
}

html,
Expand Down Expand Up @@ -220,6 +233,11 @@ body,
}

.scene-minimap {
/* Overrides React Flow's own baked-in Panel margin (15px, from its
bottom-right default position) so the minimap uses the SAME edge
inset as every other piece of canvas chrome, rather than a value
that happened to be close but came from a different system. */
margin: var(--gl-chrome-inset) !important;
background-color: var(--gl-surface-node-body);
border: 1px solid var(--gl-surface-border);
border-radius: 8px;
Expand All @@ -230,104 +248,6 @@ body,
fill-opacity: 0.55;
}

.scene-pins {
position: absolute;
left: 16px;
top: 16px;
width: 190px;
padding: 10px 12px;
background-color: var(--gl-surface-node-body);
border: 1px solid var(--gl-surface-border);
border-radius: 10px;
z-index: var(--gl-z-canvas-chrome);
}

.scene-pins-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}

.scene-pins-title {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.14em;
color: var(--gl-surface-text-muted);
}

.scene-pins-add {
font-size: 10px;
font-weight: 600;
font-family: inherit;
padding: 3px 8px;
color: var(--gl-surface-text);
background-color: var(--gl-neutral-button-background);
border: 1px solid var(--gl-neutral-button-border);
border-radius: 6px;
cursor: pointer;
}

.scene-pins-add:hover {
background-color: var(--gl-neutral-button-hover);
}

.scene-pins-empty {
margin: 4px 0 0;
font-size: 11px;
color: var(--gl-surface-text-muted);
}

.scene-pins-list {
margin: 0;
padding: 0;
list-style: none;
}

.scene-pins-row {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 0;
}

.scene-pins-jump {
flex: 1;
text-align: left;
font-size: 11px;
font-family: inherit;
padding: 4px 6px;
color: var(--gl-surface-text);
background: transparent;
border: none;
border-radius: 5px;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.scene-pins-jump:hover {
background-color: var(--gl-neutral-button-hover);
}

.scene-pins-remove {
font-size: 12px;
font-family: inherit;
line-height: 1;
padding: 2px 6px;
color: var(--gl-surface-text-muted);
background: transparent;
border: none;
border-radius: 5px;
cursor: pointer;
}

.scene-pins-remove:hover {
color: var(--gl-surface-text);
background-color: var(--gl-neutral-button-hover);
}

.scene-node-handle {
width: 8px;
height: 8px;
Expand Down Expand Up @@ -1045,15 +965,15 @@ body,

.app-popover-layer {
position: absolute;
top: 8px;
right: 16px;
top: var(--gl-chrome-inset);
right: var(--gl-chrome-inset);
z-index: var(--gl-z-app-layer);
}

.app-plugins-layer {
position: absolute;
top: 8px;
right: 16px;
top: var(--gl-chrome-inset);
right: var(--gl-chrome-inset);
z-index: var(--gl-z-app-layer);
}

Expand Down Expand Up @@ -1566,8 +1486,8 @@ body,

.app-token-counter-layer {
position: absolute;
bottom: 16px;
left: 16px;
bottom: var(--gl-chrome-inset);
left: var(--gl-chrome-inset);
z-index: var(--gl-z-canvas-chrome);
}

Expand Down Expand Up @@ -1607,7 +1527,7 @@ body,

.app-notification-layer {
position: absolute;
top: 16px;
top: var(--gl-chrome-inset);
left: 50%;
transform: translateX(-50%);
z-index: var(--gl-z-notification);
Expand Down Expand Up @@ -1647,8 +1567,8 @@ body,

.app-pins-layer {
position: absolute;
top: 8px;
left: 16px;
top: var(--gl-chrome-inset);
left: var(--gl-chrome-inset);
z-index: var(--gl-z-app-layer);
}

Expand Down Expand Up @@ -1878,7 +1798,7 @@ body,

.app-search-layer {
position: absolute;
top: 8px;
top: var(--gl-chrome-inset);
left: 50%;
transform: translateX(-50%);
z-index: var(--gl-z-search);
Expand Down
Loading