should work, just need to debug

This commit is contained in:
Andrew Pareles 2025-02-22 00:40:46 -08:00
parent 1079893527
commit 63b71dec24
5 changed files with 144 additions and 126 deletions

View file

@ -121,19 +121,19 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num
}
type AcceptRejectAllState = 'idle' | 'acceptRejectAll' | 'streaming'
export type URIStreamState = 'idle' | 'acceptRejectAll' | 'streaming'
export type StartApplyingOpts = {
from: 'QuickEdit';
type: 'rewrite';
diffareaid: number; // id of the CtrlK area (contains text selection)
chatCodeBoxId: string | null;
chatApplyBoxId: string | null;
} | {
from: 'ClickApply';
type: 'searchReplace' | 'rewrite';
applyStr: string;
chatCodeBoxId: string | null;
chatApplyBoxId: string | null;
}
@ -177,6 +177,7 @@ type CommonZoneProps = {
type CtrlKZone = {
type: 'CtrlKZone';
originalCode?: undefined;
chatApplyBoxId?: undefined;
editorId: string; // the editor the input lives on
@ -196,11 +197,11 @@ type DiffZone = {
type: 'DiffZone',
originalCode: string;
_diffOfId: Record<string, Diff>; // diffid -> diff in this DiffArea
chatApplyBoxId: string | null;
_streamState: {
isStreaming: true;
streamRequestIdRef: { current: string | null };
line: number;
codeBoxId: string | null;
} | {
isStreaming: false;
streamRequestIdRef?: undefined;
@ -219,6 +220,7 @@ type TrackingZone<T> = {
originalCode?: undefined;
editorId?: undefined;
_removeStylesFns?: undefined;
chatApplyBoxId?: undefined;
} & CommonZoneProps
@ -232,6 +234,7 @@ const diffAreaSnapshotKeys = [
'startLine',
'endLine',
'editorId',
'chatApplyBoxId',
] as const satisfies (keyof DiffArea)[]
@ -256,6 +259,7 @@ export interface IEditCodeService {
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
removeCtrlKZone(opts: { diffareaid: number }): void;
removeDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }): void;
// CtrlKZone streaming state
isCtrlKZoneStreaming(opts: { diffareaid: number }): boolean;
@ -263,9 +267,9 @@ export interface IEditCodeService {
onDidChangeCtrlKZoneStreaming: Event<{ uri: URI; diffareaid: number }>;
// // DiffZone codeBoxId streaming state
isCodeBoxIdStreaming(opts: { codeBoxId: string }): boolean;
interruptCodeBoxId(opts: { codeBoxId: string }): void;
onDidChangeCodeBoxIdStreaming: Event<{ codeBoxId: string }>;
getURIStreamState(opts: { uri: URI | null }): URIStreamState;
interruptURIStreaming(opts: { uri: URI }): void;
onDidChangeURIStreamState: Event<{ uri: URI; state: URIStreamState }>;
// testDiffs(): void;
}
@ -291,8 +295,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>();
onDidChangeCtrlKZoneStreaming = this._onDidChangeCtrlKZoneStreaming.event
private readonly _onDidChangeCodeBoxIdStreaming = new Emitter<{ uri: URI; diffareaid: number; codeBoxId: string }>();
onDidChangeCodeBoxIdStreaming = this._onDidChangeCodeBoxIdStreaming.event
private readonly _onDidChangeURIStreamState = new Emitter<{ uri: URI; state: URIStreamState }>();
onDidChangeURIStreamState = this._onDidChangeURIStreamState.event
constructor(
// @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right
@ -327,36 +334,30 @@ class EditCodeService extends Disposable implements IEditCodeService {
})
)
// when a stream starts or ends, add/remove the accept|reject UI
let _removeAcceptRejectAllUI: (() => void) | null = null
// when a stream starts or ends, fire the event for onDidChangeURIStreamState
let prevStreamState = this.getURIStreamState({ uri: model.uri })
const updateAcceptRejectAllUI = () => {
const uri = model.uri
const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()]
.map(diffareaid => this.diffAreaOfId[diffareaid])
.filter(diffArea => !!diffArea && diffArea.type === 'DiffZone')
const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming)
const state = this.getURIStreamState({ uri: model.uri })
if (prevStreamState === state) return
this._onDidChangeURIStreamState.fire({ uri: model.uri, state })
}
const state: AcceptRejectAllState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll')
// add/remove the accept|reject UI
let _removeAcceptRejectAllUI: (() => void) | null = null
this._register(this._onDidChangeURIStreamState.event(({ uri: uri_ }) => {
if (uri_.fsPath !== model.uri.fsPath) return
const state = this.getURIStreamState({ uri: model.uri })
if (state === 'acceptRejectAll' && !_removeAcceptRejectAllUI) {
_removeAcceptRejectAllUI = this._addAcceptRejectAllUI(uri) ?? null
_removeAcceptRejectAllUI = this._addAcceptRejectAllUI(model.uri) ?? null
} else {
_removeAcceptRejectAllUI?.()
_removeAcceptRejectAllUI = null
}
}
}))
this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() }))
this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() }))
// codeBoxId
this._register(this._onDidChangeDiffZoneStreaming.event(({ diffareaid }) => {
const diffZone = this.diffAreaOfId[diffareaid]
if (diffZone?.type !== 'DiffZone') return
if (!diffZone._streamState.isStreaming) return
const { codeBoxId } = diffZone._streamState
if (codeBoxId === null) return
this._onDidChangeCodeBoxIdStreaming.fire({ uri: model.uri, codeBoxId, diffareaid })
}))
}
// initialize all existing models + initialize when a new model mounts
@ -505,7 +506,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
const buttonsWidget = new AcceptAllRejectAllWidget({
editor,
onAcceptAll: () => {
this.removeDiffAreas({ uri, behavior: 'keep', removeCtrlKs: false })
this.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
this._metricsService.capture('Accept All', {})
},
onRejectAll: () => {
@ -1266,7 +1267,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined {
const { from, chatCodeBoxId } = opts
const { from, chatApplyBoxId } = opts
let startLine: number
let endLine: number
@ -1318,6 +1319,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
const adding: Omit<DiffZone, 'diffareaid'> = {
type: 'DiffZone',
chatApplyBoxId,
originalCode,
startLine,
endLine,
@ -1326,7 +1328,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
isStreaming: true,
streamRequestIdRef,
line: startLine,
codeBoxId: chatCodeBoxId,
},
_diffOfId: {}, // added later
_removeStylesFns: new Set(),
@ -1452,7 +1453,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }) {
const { applyStr, chatCodeBoxId } = opts
const { applyStr, chatApplyBoxId } = opts
const uri_ = this._getActiveEditorURI()
if (!uri_) return
@ -1493,6 +1494,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
const adding: Omit<DiffZone, 'diffareaid'> = {
type: 'DiffZone',
chatApplyBoxId,
originalCode: originalFileCode,
startLine,
endLine,
@ -1501,7 +1503,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
isStreaming: true,
streamRequestIdRef,
line: startLine,
codeBoxId: chatCodeBoxId,
},
_diffOfId: {}, // added later
_removeStylesFns: new Set(),
@ -1555,7 +1556,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
this._deleteTrackingZone(trackingZone)
onFinishEdit()
shouldSendAnotherMessage = false
}
// refresh now in case onText takes a while to get 1st message
@ -1746,7 +1746,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
_interruptDiffZoneStreaming({ diffareaid }: { diffareaid: number }) {
_interruptSingleDiffZoneStreaming({ diffareaid }: { diffareaid: number }) {
const diffZone = this.diffAreaOfId[diffareaid]
if (diffZone?.type !== 'DiffZone') return
if (!diffZone._streamState.isStreaming) return
@ -1774,35 +1774,33 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (!linkedStreamingDiffZone) return
if (linkedStreamingDiffZone.type !== 'DiffZone') return
this._interruptDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid })
this._interruptSingleDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid })
}
isCodeBoxIdStreaming({ codeBoxId }: { codeBoxId: string }) {
// brute force is OK for now
for (const diffareaid in this.diffAreaOfId) {
const diffArea = this.diffAreaOfId[diffareaid]
if (!diffArea) continue
if (diffArea.type !== 'DiffZone') continue
if (!diffArea._streamState.isStreaming) continue
if (diffArea._streamState.codeBoxId === codeBoxId) return true
}
return false
getURIStreamState = ({ uri }: { uri: URI | null }) => {
if (uri === null) return 'idle'
const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()]
.map(diffareaid => this.diffAreaOfId[diffareaid])
.filter(diffArea => !!diffArea && diffArea.type === 'DiffZone')
const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming)
const state: URIStreamState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll')
return state
}
interruptCodeBoxId({ codeBoxId }: { codeBoxId: string }) {
interruptURIStreaming({ uri }: { uri: URI }) {
// brute force for now is OK
for (const diffareaid in this.diffAreaOfId) {
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
const diffArea = this.diffAreaOfId[diffareaid]
if (!diffArea) continue
if (diffArea.type !== 'DiffZone') continue
if (diffArea?.type !== 'DiffZone') continue
if (!diffArea._streamState.isStreaming) continue
if (diffArea._streamState.codeBoxId === codeBoxId) {
this._interruptDiffZoneStreaming({ diffareaid: diffArea.diffareaid })
return
}
this._stopIfStreaming(diffArea)
}
this._undoHistory(uri)
}
@ -1829,7 +1827,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// remove a batch of diffareas all at once (and handle accept/reject of their diffs)
public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'keep' }) {
public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }) {
const diffareaids = this.diffAreasOfURI[uri.fsPath]
if (diffareaids.size === 0) return // do nothing
@ -1842,7 +1840,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (diffArea.type == 'DiffZone') {
if (behavior === 'reject') this._revertAndDeleteDiffZone(diffArea)
else if (behavior === 'keep') this._deleteDiffZone(diffArea)
else if (behavior === 'accept') this._deleteDiffZone(diffArea)
}
else if (diffArea.type === 'CtrlKZone' && removeCtrlKs) {
this._deleteCtrlKZone(diffArea)

View file

@ -1,7 +1,8 @@
import { useState, useEffect, useCallback } from 'react'
import { useAccessor, useCodeBoxIdStreamingState, useSettingsState } from '../util/services.js'
import { useAccessor, useURIStreamState, useSettingsState } from '../util/services.js'
import { useRefState } from '../util/helpers.js'
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
import { URI } from '../../../../../../../base/common/uri.js'
enum CopyButtonText {
Idle = 'Copy',
@ -44,80 +45,71 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
}
const useStreamStateRef = ({ codeBoxId }: { codeBoxId: string | null }) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const [isStreamingRef, setIsStreamingRef] = useRefState(editCodeService.isCodeBoxIdStreaming({ codeBoxId }))
useCodeBoxIdStreamingState(useCallback((codeBoxId2, isStreaming) => {
if (codeBoxId !== codeBoxId2) return
setIsStreamingRef(isStreaming)
}, [codeBoxId, setIsStreamingRef]))
return [isStreamingRef, setIsStreamingRef] as const
// state persisted for duration of react only
const streamingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
const useStreamingURIOfApplyBoxId = (applyBoxId: string | null) => {
const [_, ss] = useState(0)
const uri = applyBoxId === null ? null : streamingURIOfApplyBoxIdRef.current[applyBoxId]
const setUri = useCallback((uri: URI | null) => {
if (applyBoxId === null) return
ss(c => c + 1)
if (uri === null) {
delete streamingURIOfApplyBoxIdRef.current[applyBoxId]
}
else {
streamingURIOfApplyBoxIdRef.current = {
...streamingURIOfApplyBoxIdRef.current,
[applyBoxId]: uri,
}
}
}, [applyBoxId])
return [uri, setUri] as const
}
export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string | null }) => {
const StopButton = ({ codeBoxId }: { codeBoxId: string }) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const metricsService = accessor.get('IMetricsService')
const settingsState = useSettingsState()
const [isStreamingRef, _] = useStreamStateRef({ codeBoxId })
return <button
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={onInterrupt}
>
Apply
</button>
}
export const ApplyBlockHoverButtons = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: string | null }) => {
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || applyBoxId === null
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const metricsService = accessor.get('IMetricsService')
const settingsState = useSettingsState()
// get streaming URI of this applyBlockId (cached in react)
const [appliedURI, setAppliedURI] = useStreamingURIOfApplyBoxId(applyBoxId)
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState)
// get stream state of this URI
const [streamStateRef, setStreamState] = useRefState(editCodeService.getURIStreamState({ uri: appliedURI ?? null }))
useURIStreamState(useCallback((uri, streamState) => {
if (appliedURI?.fsPath !== uri.fsPath) return
setStreamState(streamState)
}, [appliedURI, setStreamState]))
const [isStreamingRef, _] = useStreamStateRef({ codeBoxId })
const onSubmit = useCallback(() => {
if (isDisabled) return
if (isStreamingRef.current) return
editCodeService.startApplying({
const uri = editCodeService.startApplying({
from: 'ClickApply',
type: 'searchReplace',
applyStr: codeStr,
chatCodeBoxId: codeBoxId,
chatApplyBoxId: applyBoxId,
})
setAppliedURI(uri)
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
}, [isStreamingRef, editCodeService, codeBoxId, codeStr, metricsService])
}, [streamStateRef, setAppliedURI, editCodeService, applyBoxId, codeStr, metricsService])
const onInterrupt = useCallback(() => {
if (isStreamingRef.current) return
if (codeBoxId === null) return
editCodeService.interruptCodeBoxId({ codeBoxId, })
if (!appliedURI) return
editCodeService.interruptURIStreaming({ uri: appliedURI, })
metricsService.capture('Stop Apply', {})
}, [isStreamingRef, editCodeService, codeBoxId, metricsService])
}, [streamStateRef, editCodeService, appliedURI, metricsService])
const isSingleLine = !codeStr.includes('\n')
@ -130,11 +122,42 @@ export const ApplyBlockHoverButtons = ({ codeStr, codeBoxId }: { codeStr: string
Apply
</button>
const stopButton = <button
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={onInterrupt}
>
Stop
</button>
const acceptRejectButtons = <>
<button
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={() => {
if (!appliedURI) return
editCodeService.removeDiffAreas({ uri: appliedURI, behavior: 'accept', removeCtrlKs: false })
}}
>
Accept
</button>
<button
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={() => {
if (!appliedURI) return
editCodeService.removeDiffAreas({ uri: appliedURI, behavior: 'reject', removeCtrlKs: false })
}}
>
Reject
</button>
</>
return <>
{!isStreamingRef.current && <CopyButton codeStr={codeStr} />}
{!isStreamingRef.current && codeBoxId !== null && <ApplyButton codeBoxId={codeBoxId} codeStr={codeStr} />}
{!isStreamingRef.current && <StopButton codeStr={codeStr} />}
{streamStateRef.current !== 'streaming' && <CopyButton codeStr={codeStr} />}
{streamStateRef.current === 'idle' && !isDisabled && applyButton}
{streamStateRef.current === 'streaming' && stopButton}
{streamStateRef.current === 'acceptRejectAll' && acceptRejectButtons}
</>
}

View file

@ -14,7 +14,7 @@ import { URI } from '../../../../../../../base/common/uri.js'
type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string }
const getCodeBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => {
const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => {
return `${threadId}-${messageIdx}-${tokenIdx}`
}
@ -46,7 +46,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }:
if (t.type === "code") {
const codeBoxId = chatMessageLocation ? getCodeBoxId({
const applyBoxId = chatMessageLocation ? getApplyBoxId({
threadId: chatMessageLocation.threadId,
messageIdx: chatMessageLocation.messageIdx,
tokenIdx: tokenIdx,
@ -55,7 +55,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }:
return <BlockCode
initValue={t.text}
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
buttonsOnHover={<ApplyBlockHoverButtons codeBoxId={codeBoxId} codeStr={t.text} />}
buttonsOnHover={<ApplyBlockHoverButtons applyBoxId={applyBoxId} codeStr={t.text} />}
/>
}

View file

@ -65,7 +65,7 @@ export const QuickEditChat = ({
from: 'QuickEdit',
type: 'rewrite',
diffareaid,
chatCodeBoxId: null,
chatApplyBoxId: null,
})
}, [isStreamingRef, isDisabled, editCodeService, diffareaid])

View file

@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useCallback } from 'react'
import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
@ -24,7 +24,7 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
import { ILLMMessageService } from '../../../../../../../workbench/contrib/void/common/llmMessageService.js';
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
import { IEditCodeService } from '../../../editCodeService.js';
import { IEditCodeService, URIStreamState } from '../../../editCodeService.js';
import { IVoidUriStateService } from '../../../voidUriStateService.js';
import { IQuickEditStateService } from '../../../quickEditStateService.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
@ -43,6 +43,7 @@ import { IEnvironmentService } from '../../../../../../../platform/environment/c
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'
import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js'
import { IMetricsService } from '../../../../../../../workbench/contrib/void/common/metricsService.js'
import { URI } from '../../../../../../../base/common/uri.js'
@ -76,7 +77,7 @@ let colorThemeState: ColorScheme
const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set()
const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set()
const codeBoxIdStreamingStateListeners: Set<(codeBoxId: string, s: boolean) => void> = new Set()
const uriStreamingStateListeners: Set<(uri: URI, s: URIStreamState) => void> = new Set()
@ -183,9 +184,9 @@ export const _registerServices = (accessor: ServicesAccessor) => {
})
)
disposables.push(
editCodeService.onDidChangeCodeBoxIdStreaming(({ codeBoxId }) => {
const isStreaming = editCodeService.isCodeBoxIdStreaming({ codeBoxId })
codeBoxIdStreamingStateListeners.forEach(l => l(codeBoxId, isStreaming))
editCodeService.onDidChangeURIStreamState(({ uri }) => {
const isStreaming = editCodeService.getURIStreamState({ uri })
uriStreamingStateListeners.forEach(l => l(uri, isStreaming))
})
)
@ -362,18 +363,14 @@ export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boo
}, [listener, ctrlKZoneStreamingStateListeners])
}
export const useCodeBoxIdStreamingState = (listener: (codeBoxId: string, s: boolean) => void) => {
export const useURIStreamState = (listener: (uri: URI, s: URIStreamState) => void) => {
useEffect(() => {
codeBoxIdStreamingStateListeners.add(listener)
return () => { codeBoxIdStreamingStateListeners.delete(listener) }
}, [listener, codeBoxIdStreamingStateListeners])
uriStreamingStateListeners.add(listener)
return () => { uriStreamingStateListeners.delete(listener) }
}, [listener, uriStreamingStateListeners])
}
export const useIsDark = () => {
const [s, ss] = useState(colorThemeState)
useEffect(() => {