mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)
* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-2): 格式化 commands (79 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-5): 格式化 components其余 + hooks + tools (232 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md - README.md: 大幅重写,更详细版本历史和配置示例 - Run.ps1: 新增 Windows 启动脚本 - TODO.md: 新增包完成清单 - V6.md: 删除(架构重构规划已不适用) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复以前的问题 * fix: 修复 login 面板的问题 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,62 +1,45 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box } from '../ink.js';
|
||||
import * as React from 'react'
|
||||
import { Box } from '../ink.js'
|
||||
|
||||
type QueuedMessageContextValue = {
|
||||
isQueued: boolean;
|
||||
isFirst: boolean;
|
||||
isQueued: boolean
|
||||
isFirst: boolean
|
||||
/** Width reduction for container padding (e.g., 4 for paddingX={2}) */
|
||||
paddingWidth: number;
|
||||
};
|
||||
const QueuedMessageContext = React.createContext<QueuedMessageContextValue | undefined>(undefined);
|
||||
export function useQueuedMessage() {
|
||||
return React.useContext(QueuedMessageContext);
|
||||
paddingWidth: number
|
||||
}
|
||||
const PADDING_X = 2;
|
||||
|
||||
const QueuedMessageContext = React.createContext<
|
||||
QueuedMessageContextValue | undefined
|
||||
>(undefined)
|
||||
|
||||
export function useQueuedMessage(): QueuedMessageContextValue | undefined {
|
||||
return React.useContext(QueuedMessageContext)
|
||||
}
|
||||
|
||||
const PADDING_X = 2
|
||||
|
||||
type Props = {
|
||||
isFirst: boolean;
|
||||
useBriefLayout?: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
export function QueuedMessageProvider(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
isFirst,
|
||||
useBriefLayout,
|
||||
children
|
||||
} = t0;
|
||||
const padding = useBriefLayout ? 0 : PADDING_X;
|
||||
const t1 = padding * 2;
|
||||
let t2;
|
||||
if ($[0] !== isFirst || $[1] !== t1) {
|
||||
t2 = {
|
||||
isQueued: true,
|
||||
isFirst,
|
||||
paddingWidth: t1
|
||||
};
|
||||
$[0] = isFirst;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const value = t2;
|
||||
let t3;
|
||||
if ($[3] !== children || $[4] !== padding) {
|
||||
t3 = <Box paddingX={padding}>{children}</Box>;
|
||||
$[3] = children;
|
||||
$[4] = padding;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== t3 || $[7] !== value) {
|
||||
t4 = <QueuedMessageContext.Provider value={value}>{t3}</QueuedMessageContext.Provider>;
|
||||
$[6] = t3;
|
||||
$[7] = value;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
return t4;
|
||||
isFirst: boolean
|
||||
useBriefLayout?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function QueuedMessageProvider({
|
||||
isFirst,
|
||||
useBriefLayout,
|
||||
children,
|
||||
}: Props): React.ReactNode {
|
||||
// Brief mode already indents via paddingLeft in HighlightedThinkingText /
|
||||
// BriefTool UI — adding paddingX here would double-indent the queue.
|
||||
const padding = useBriefLayout ? 0 : PADDING_X
|
||||
const value = React.useMemo(
|
||||
() => ({ isQueued: true, isFirst, paddingWidth: padding * 2 }),
|
||||
[isFirst, padding],
|
||||
)
|
||||
|
||||
return (
|
||||
<QueuedMessageContext.Provider value={value}>
|
||||
<Box paddingX={padding}>{children}</Box>
|
||||
</QueuedMessageContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import type { FpsMetrics } from '../utils/fpsTracker.js';
|
||||
type FpsMetricsGetter = () => FpsMetrics | undefined;
|
||||
const FpsMetricsContext = createContext<FpsMetricsGetter | undefined>(undefined);
|
||||
import React, { createContext, useContext } from 'react'
|
||||
import type { FpsMetrics } from '../utils/fpsTracker.js'
|
||||
|
||||
type FpsMetricsGetter = () => FpsMetrics | undefined
|
||||
|
||||
const FpsMetricsContext = createContext<FpsMetricsGetter | undefined>(undefined)
|
||||
|
||||
type Props = {
|
||||
getFpsMetrics: FpsMetricsGetter;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
export function FpsMetricsProvider(t0) {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
getFpsMetrics,
|
||||
children
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== children || $[1] !== getFpsMetrics) {
|
||||
t1 = <FpsMetricsContext.Provider value={getFpsMetrics}>{children}</FpsMetricsContext.Provider>;
|
||||
$[0] = children;
|
||||
$[1] = getFpsMetrics;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
getFpsMetrics: FpsMetricsGetter
|
||||
children: React.ReactNode
|
||||
}
|
||||
export function useFpsMetrics() {
|
||||
return useContext(FpsMetricsContext);
|
||||
|
||||
export function FpsMetricsProvider({
|
||||
getFpsMetrics,
|
||||
children,
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<FpsMetricsContext.Provider value={getFpsMetrics}>
|
||||
{children}
|
||||
</FpsMetricsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useFpsMetrics(): FpsMetricsGetter | undefined {
|
||||
return useContext(FpsMetricsContext)
|
||||
}
|
||||
|
||||
@@ -1,37 +1,25 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { Mailbox } from '../utils/mailbox.js';
|
||||
const MailboxContext = createContext<Mailbox | undefined>(undefined);
|
||||
import React, { createContext, useContext, useMemo } from 'react'
|
||||
import { Mailbox } from '../utils/mailbox.js'
|
||||
|
||||
const MailboxContext = createContext<Mailbox | undefined>(undefined)
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
export function MailboxProvider(t0) {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
children
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = new Mailbox();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const mailbox = t1;
|
||||
let t2;
|
||||
if ($[1] !== children) {
|
||||
t2 = <MailboxContext.Provider value={mailbox}>{children}</MailboxContext.Provider>;
|
||||
$[1] = children;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
children: React.ReactNode
|
||||
}
|
||||
export function useMailbox() {
|
||||
const mailbox = useContext(MailboxContext);
|
||||
|
||||
export function MailboxProvider({ children }: Props): React.ReactNode {
|
||||
const mailbox = useMemo(() => new Mailbox(), [])
|
||||
return (
|
||||
<MailboxContext.Provider value={mailbox}>
|
||||
{children}
|
||||
</MailboxContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useMailbox(): Mailbox {
|
||||
const mailbox = useContext(MailboxContext)
|
||||
if (!mailbox) {
|
||||
throw new Error("useMailbox must be used within a MailboxProvider");
|
||||
throw new Error('useMailbox must be used within a MailboxProvider')
|
||||
}
|
||||
return mailbox;
|
||||
return mailbox
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { createContext, type RefObject, useContext } from 'react';
|
||||
import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js';
|
||||
import { createContext, type RefObject, useContext } from 'react'
|
||||
import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js'
|
||||
|
||||
/**
|
||||
* Set by FullscreenLayout when rendering content in its `modal` slot —
|
||||
@@ -20,13 +19,14 @@ import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js';
|
||||
* null = not inside the modal slot.
|
||||
*/
|
||||
type ModalCtx = {
|
||||
rows: number;
|
||||
columns: number;
|
||||
scrollRef: RefObject<ScrollBoxHandle | null> | null;
|
||||
};
|
||||
export const ModalContext = createContext<ModalCtx | null>(null);
|
||||
export function useIsInsideModal() {
|
||||
return useContext(ModalContext) !== null;
|
||||
rows: number
|
||||
columns: number
|
||||
scrollRef: RefObject<ScrollBoxHandle | null> | null
|
||||
}
|
||||
export const ModalContext = createContext<ModalCtx | null>(null)
|
||||
|
||||
export function useIsInsideModal(): boolean {
|
||||
return useContext(ModalContext) !== null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,23 +35,14 @@ export function useIsInsideModal() {
|
||||
* component caps its visible content height — the modal's inner area is
|
||||
* smaller than the terminal.
|
||||
*/
|
||||
export function useModalOrTerminalSize(fallback) {
|
||||
const $ = _c(3);
|
||||
const ctx = useContext(ModalContext);
|
||||
let t0;
|
||||
if ($[0] !== ctx || $[1] !== fallback) {
|
||||
t0 = ctx ? {
|
||||
rows: ctx.rows,
|
||||
columns: ctx.columns
|
||||
} : fallback;
|
||||
$[0] = ctx;
|
||||
$[1] = fallback;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
return t0;
|
||||
export function useModalOrTerminalSize(fallback: {
|
||||
rows: number
|
||||
columns: number
|
||||
}): { rows: number; columns: number } {
|
||||
const ctx = useContext(ModalContext)
|
||||
return ctx ? { rows: ctx.rows, columns: ctx.columns } : fallback
|
||||
}
|
||||
export function useModalScrollRef() {
|
||||
return useContext(ModalContext)?.scrollRef ?? null;
|
||||
|
||||
export function useModalScrollRef(): RefObject<ScrollBoxHandle | null> | null {
|
||||
return useContext(ModalContext)?.scrollRef ?? null
|
||||
}
|
||||
|
||||
@@ -1,216 +1,288 @@
|
||||
import type * as React from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useAppStateStore, useSetAppState } from 'src/state/AppState.js';
|
||||
import type { Theme } from '../utils/theme.js';
|
||||
type Priority = 'low' | 'medium' | 'high' | 'immediate';
|
||||
import type * as React from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useAppStateStore, useSetAppState } from 'src/state/AppState.js'
|
||||
import type { Theme } from '../utils/theme.js'
|
||||
|
||||
type Priority = 'low' | 'medium' | 'high' | 'immediate'
|
||||
|
||||
type BaseNotification = {
|
||||
key: string;
|
||||
key: string
|
||||
/**
|
||||
* Keys of notifications that this notification invalidates.
|
||||
* If a notification is invalidated, it will be removed from the queue
|
||||
* and, if currently displayed, cleared immediately.
|
||||
*/
|
||||
invalidates?: string[];
|
||||
priority: Priority;
|
||||
timeoutMs?: number;
|
||||
invalidates?: string[]
|
||||
priority: Priority
|
||||
timeoutMs?: number
|
||||
/**
|
||||
* Combine notifications with the same key, like Array.reduce().
|
||||
* Called as fold(accumulator, incoming) when a notification with a matching
|
||||
* key already exists in the queue or is currently displayed.
|
||||
* Returns the merged notification (should carry fold forward for future merges).
|
||||
*/
|
||||
fold?: (accumulator: Notification, incoming: Notification) => Notification;
|
||||
};
|
||||
fold?: (accumulator: Notification, incoming: Notification) => Notification
|
||||
}
|
||||
|
||||
type TextNotification = BaseNotification & {
|
||||
text: string;
|
||||
color?: keyof Theme;
|
||||
};
|
||||
text: string
|
||||
color?: keyof Theme
|
||||
}
|
||||
|
||||
type JSXNotification = BaseNotification & {
|
||||
jsx: React.ReactNode;
|
||||
};
|
||||
type AddNotificationFn = (content: Notification) => void;
|
||||
type RemoveNotificationFn = (key: string) => void;
|
||||
export type Notification = TextNotification | JSXNotification;
|
||||
const DEFAULT_TIMEOUT_MS = 8000;
|
||||
jsx: React.ReactNode
|
||||
}
|
||||
|
||||
type AddNotificationFn = (content: Notification) => void
|
||||
type RemoveNotificationFn = (key: string) => void
|
||||
|
||||
export type Notification = TextNotification | JSXNotification
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 8000
|
||||
|
||||
// Track current timeout to clear it when immediate notifications arrive
|
||||
let currentTimeoutId: NodeJS.Timeout | null = null;
|
||||
let currentTimeoutId: NodeJS.Timeout | null = null
|
||||
|
||||
export function useNotifications(): {
|
||||
addNotification: AddNotificationFn;
|
||||
removeNotification: RemoveNotificationFn;
|
||||
addNotification: AddNotificationFn
|
||||
removeNotification: RemoveNotificationFn
|
||||
} {
|
||||
const store = useAppStateStore();
|
||||
const setAppState = useSetAppState();
|
||||
const store = useAppStateStore()
|
||||
const setAppState = useSetAppState()
|
||||
|
||||
// Process queue when current notification finishes or queue changes
|
||||
const processQueue = useCallback(() => {
|
||||
setAppState(prev => {
|
||||
const next = getNext(prev.notifications.queue);
|
||||
const next = getNext(prev.notifications.queue)
|
||||
if (prev.notifications.current !== null || !next) {
|
||||
return prev;
|
||||
return prev
|
||||
}
|
||||
currentTimeoutId = setTimeout((setAppState, nextKey, processQueue) => {
|
||||
currentTimeoutId = null;
|
||||
setAppState(prev => {
|
||||
// Compare by key instead of reference to handle re-created notifications
|
||||
if (prev.notifications.current?.key !== nextKey) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
notifications: {
|
||||
queue: prev.notifications.queue,
|
||||
current: null
|
||||
|
||||
currentTimeoutId = setTimeout(
|
||||
(setAppState, nextKey, processQueue) => {
|
||||
currentTimeoutId = null
|
||||
setAppState(prev => {
|
||||
// Compare by key instead of reference to handle re-created notifications
|
||||
if (prev.notifications.current?.key !== nextKey) {
|
||||
return prev
|
||||
}
|
||||
};
|
||||
});
|
||||
processQueue();
|
||||
}, next.timeoutMs ?? DEFAULT_TIMEOUT_MS, setAppState, next.key, processQueue);
|
||||
return {
|
||||
...prev,
|
||||
notifications: {
|
||||
queue: prev.notifications.queue,
|
||||
current: null,
|
||||
},
|
||||
}
|
||||
})
|
||||
processQueue()
|
||||
},
|
||||
next.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
setAppState,
|
||||
next.key,
|
||||
processQueue,
|
||||
)
|
||||
|
||||
return {
|
||||
...prev,
|
||||
notifications: {
|
||||
queue: prev.notifications.queue.filter(_ => _ !== next),
|
||||
current: next
|
||||
}
|
||||
};
|
||||
});
|
||||
}, [setAppState]);
|
||||
const addNotification = useCallback<AddNotificationFn>((notif: Notification) => {
|
||||
// Handle immediate priority notifications
|
||||
if (notif.priority === 'immediate') {
|
||||
// Clear any existing timeout since we're showing a new immediate notification
|
||||
if (currentTimeoutId) {
|
||||
clearTimeout(currentTimeoutId);
|
||||
currentTimeoutId = null;
|
||||
current: next,
|
||||
},
|
||||
}
|
||||
})
|
||||
}, [setAppState])
|
||||
|
||||
// Set up timeout for the immediate notification
|
||||
currentTimeoutId = setTimeout((setAppState, notif, processQueue) => {
|
||||
currentTimeoutId = null;
|
||||
setAppState(prev => {
|
||||
// Compare by key instead of reference to handle re-created notifications
|
||||
if (prev.notifications.current?.key !== notif.key) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
notifications: {
|
||||
queue: prev.notifications.queue.filter(_ => !notif.invalidates?.includes(_.key)),
|
||||
current: null
|
||||
}
|
||||
};
|
||||
});
|
||||
processQueue();
|
||||
}, notif.timeoutMs ?? DEFAULT_TIMEOUT_MS, setAppState, notif, processQueue);
|
||||
|
||||
// Show the immediate notification right away
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
notifications: {
|
||||
current: notif,
|
||||
queue:
|
||||
// Only re-queue the current notification if it's not immediate
|
||||
[...(prev.notifications.current ? [prev.notifications.current] : []), ...prev.notifications.queue].filter(_ => _.priority !== 'immediate' && !notif.invalidates?.includes(_.key))
|
||||
const addNotification = useCallback<AddNotificationFn>(
|
||||
(notif: Notification) => {
|
||||
// Handle immediate priority notifications
|
||||
if (notif.priority === 'immediate') {
|
||||
// Clear any existing timeout since we're showing a new immediate notification
|
||||
if (currentTimeoutId) {
|
||||
clearTimeout(currentTimeoutId)
|
||||
currentTimeoutId = null
|
||||
}
|
||||
}));
|
||||
return; // IMPORTANT: Exit addNotification for immediate notifications
|
||||
}
|
||||
|
||||
// Handle non-immediate notifications
|
||||
setAppState(prev => {
|
||||
// Check if we can fold into an existing notification with the same key
|
||||
if (notif.fold) {
|
||||
// Fold into current notification if keys match
|
||||
if (prev.notifications.current?.key === notif.key) {
|
||||
const folded = notif.fold(prev.notifications.current, notif);
|
||||
// Reset timeout for the folded notification
|
||||
if (currentTimeoutId) {
|
||||
clearTimeout(currentTimeoutId);
|
||||
currentTimeoutId = null;
|
||||
}
|
||||
currentTimeoutId = setTimeout((setAppState, foldedKey, processQueue) => {
|
||||
currentTimeoutId = null;
|
||||
setAppState(p => {
|
||||
if (p.notifications.current?.key !== foldedKey) {
|
||||
return p;
|
||||
// Set up timeout for the immediate notification
|
||||
currentTimeoutId = setTimeout(
|
||||
(setAppState, notif, processQueue) => {
|
||||
currentTimeoutId = null
|
||||
setAppState(prev => {
|
||||
// Compare by key instead of reference to handle re-created notifications
|
||||
if (prev.notifications.current?.key !== notif.key) {
|
||||
return prev
|
||||
}
|
||||
return {
|
||||
...p,
|
||||
...prev,
|
||||
notifications: {
|
||||
queue: p.notifications.queue,
|
||||
current: null
|
||||
}
|
||||
};
|
||||
});
|
||||
processQueue();
|
||||
}, folded.timeoutMs ?? DEFAULT_TIMEOUT_MS, setAppState, folded.key, processQueue);
|
||||
return {
|
||||
...prev,
|
||||
notifications: {
|
||||
current: folded,
|
||||
queue: prev.notifications.queue
|
||||
queue: prev.notifications.queue.filter(
|
||||
_ => !notif.invalidates?.includes(_.key),
|
||||
),
|
||||
current: null,
|
||||
},
|
||||
}
|
||||
})
|
||||
processQueue()
|
||||
},
|
||||
notif.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
setAppState,
|
||||
notif,
|
||||
processQueue,
|
||||
)
|
||||
|
||||
// Show the immediate notification right away
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
notifications: {
|
||||
current: notif,
|
||||
queue:
|
||||
// Only re-queue the current notification if it's not immediate
|
||||
[
|
||||
...(prev.notifications.current
|
||||
? [prev.notifications.current]
|
||||
: []),
|
||||
...prev.notifications.queue,
|
||||
].filter(
|
||||
_ =>
|
||||
_.priority !== 'immediate' &&
|
||||
!notif.invalidates?.includes(_.key),
|
||||
),
|
||||
},
|
||||
}))
|
||||
return // IMPORTANT: Exit addNotification for immediate notifications
|
||||
}
|
||||
|
||||
// Handle non-immediate notifications
|
||||
setAppState(prev => {
|
||||
// Check if we can fold into an existing notification with the same key
|
||||
if (notif.fold) {
|
||||
// Fold into current notification if keys match
|
||||
if (prev.notifications.current?.key === notif.key) {
|
||||
const folded = notif.fold(prev.notifications.current, notif)
|
||||
// Reset timeout for the folded notification
|
||||
if (currentTimeoutId) {
|
||||
clearTimeout(currentTimeoutId)
|
||||
currentTimeoutId = null
|
||||
}
|
||||
};
|
||||
}
|
||||
currentTimeoutId = setTimeout(
|
||||
(setAppState, foldedKey, processQueue) => {
|
||||
currentTimeoutId = null
|
||||
setAppState(p => {
|
||||
if (p.notifications.current?.key !== foldedKey) {
|
||||
return p
|
||||
}
|
||||
return {
|
||||
...p,
|
||||
notifications: {
|
||||
queue: p.notifications.queue,
|
||||
current: null,
|
||||
},
|
||||
}
|
||||
})
|
||||
processQueue()
|
||||
},
|
||||
folded.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
setAppState,
|
||||
folded.key,
|
||||
processQueue,
|
||||
)
|
||||
|
||||
// Fold into queued notification if keys match
|
||||
const queueIdx = prev.notifications.queue.findIndex(_ => _.key === notif.key);
|
||||
if (queueIdx !== -1) {
|
||||
const folded = notif.fold(prev.notifications.queue[queueIdx]!, notif);
|
||||
const newQueue = [...prev.notifications.queue];
|
||||
newQueue[queueIdx] = folded;
|
||||
return {
|
||||
...prev,
|
||||
notifications: {
|
||||
current: prev.notifications.current,
|
||||
queue: newQueue
|
||||
return {
|
||||
...prev,
|
||||
notifications: {
|
||||
current: folded,
|
||||
queue: prev.notifications.queue,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only add to queue if not already present (prevent duplicates)
|
||||
const queuedKeys = new Set(prev.notifications.queue.map(_ => _.key));
|
||||
const shouldAdd = !queuedKeys.has(notif.key) && prev.notifications.current?.key !== notif.key;
|
||||
if (!shouldAdd) return prev;
|
||||
const invalidatesCurrent = prev.notifications.current !== null && notif.invalidates?.includes(prev.notifications.current.key);
|
||||
if (invalidatesCurrent && currentTimeoutId) {
|
||||
clearTimeout(currentTimeoutId);
|
||||
currentTimeoutId = null;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
notifications: {
|
||||
current: invalidatesCurrent ? null : prev.notifications.current,
|
||||
queue: [...prev.notifications.queue.filter(_ => _.priority !== 'immediate' && !notif.invalidates?.includes(_.key)), notif]
|
||||
// Fold into queued notification if keys match
|
||||
const queueIdx = prev.notifications.queue.findIndex(
|
||||
_ => _.key === notif.key,
|
||||
)
|
||||
if (queueIdx !== -1) {
|
||||
const folded = notif.fold(
|
||||
prev.notifications.queue[queueIdx]!,
|
||||
notif,
|
||||
)
|
||||
const newQueue = [...prev.notifications.queue]
|
||||
newQueue[queueIdx] = folded
|
||||
return {
|
||||
...prev,
|
||||
notifications: {
|
||||
current: prev.notifications.current,
|
||||
queue: newQueue,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Process queue after adding the notification
|
||||
processQueue();
|
||||
}, [setAppState, processQueue]);
|
||||
const removeNotification = useCallback<RemoveNotificationFn>((key: string) => {
|
||||
setAppState(prev => {
|
||||
const isCurrent = prev.notifications.current?.key === key;
|
||||
const inQueue = prev.notifications.queue.some(n => n.key === key);
|
||||
if (!isCurrent && !inQueue) {
|
||||
return prev;
|
||||
}
|
||||
if (isCurrent && currentTimeoutId) {
|
||||
clearTimeout(currentTimeoutId);
|
||||
currentTimeoutId = null;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
notifications: {
|
||||
current: isCurrent ? null : prev.notifications.current,
|
||||
queue: prev.notifications.queue.filter(n => n.key !== key)
|
||||
// Only add to queue if not already present (prevent duplicates)
|
||||
const queuedKeys = new Set(prev.notifications.queue.map(_ => _.key))
|
||||
const shouldAdd =
|
||||
!queuedKeys.has(notif.key) &&
|
||||
prev.notifications.current?.key !== notif.key
|
||||
|
||||
if (!shouldAdd) return prev
|
||||
|
||||
const invalidatesCurrent =
|
||||
prev.notifications.current !== null &&
|
||||
notif.invalidates?.includes(prev.notifications.current.key)
|
||||
|
||||
if (invalidatesCurrent && currentTimeoutId) {
|
||||
clearTimeout(currentTimeoutId)
|
||||
currentTimeoutId = null
|
||||
}
|
||||
};
|
||||
});
|
||||
processQueue();
|
||||
}, [setAppState, processQueue]);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
notifications: {
|
||||
current: invalidatesCurrent ? null : prev.notifications.current,
|
||||
queue: [
|
||||
...prev.notifications.queue.filter(
|
||||
_ =>
|
||||
_.priority !== 'immediate' &&
|
||||
!notif.invalidates?.includes(_.key),
|
||||
),
|
||||
notif,
|
||||
],
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Process queue after adding the notification
|
||||
processQueue()
|
||||
},
|
||||
[setAppState, processQueue],
|
||||
)
|
||||
|
||||
const removeNotification = useCallback<RemoveNotificationFn>(
|
||||
(key: string) => {
|
||||
setAppState(prev => {
|
||||
const isCurrent = prev.notifications.current?.key === key
|
||||
const inQueue = prev.notifications.queue.some(n => n.key === key)
|
||||
|
||||
if (!isCurrent && !inQueue) {
|
||||
return prev
|
||||
}
|
||||
|
||||
if (isCurrent && currentTimeoutId) {
|
||||
clearTimeout(currentTimeoutId)
|
||||
currentTimeoutId = null
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
notifications: {
|
||||
current: isCurrent ? null : prev.notifications.current,
|
||||
queue: prev.notifications.queue.filter(n => n.key !== key),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
processQueue()
|
||||
},
|
||||
[setAppState, processQueue],
|
||||
)
|
||||
|
||||
// Process queue on mount if there are notifications in the initial state.
|
||||
// Imperative read (not useAppState) — a subscription in a mount-only
|
||||
@@ -219,21 +291,22 @@ export function useNotifications(): {
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: mount-only effect, store is a stable context ref
|
||||
useEffect(() => {
|
||||
if (store.getState().notifications.queue.length > 0) {
|
||||
processQueue();
|
||||
processQueue()
|
||||
}
|
||||
}, []);
|
||||
return {
|
||||
addNotification,
|
||||
removeNotification
|
||||
};
|
||||
}, [])
|
||||
|
||||
return { addNotification, removeNotification }
|
||||
}
|
||||
|
||||
const PRIORITIES: Record<Priority, number> = {
|
||||
immediate: 0,
|
||||
high: 1,
|
||||
medium: 2,
|
||||
low: 3
|
||||
};
|
||||
export function getNext(queue: Notification[]): Notification | undefined {
|
||||
if (queue.length === 0) return undefined;
|
||||
return queue.reduce((min, n) => PRIORITIES[n.priority] < PRIORITIES[min.priority] ? n : min);
|
||||
low: 3,
|
||||
}
|
||||
export function getNext(queue: Notification[]): Notification | undefined {
|
||||
if (queue.length === 0) return undefined
|
||||
return queue.reduce((min, n) =>
|
||||
PRIORITIES[n.priority] < PRIORITIES[min.priority] ? n : min,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
/**
|
||||
* Overlay tracking for Escape key coordination.
|
||||
*
|
||||
@@ -13,12 +12,12 @@ import { c as _c } from "react/compiler-runtime";
|
||||
* The hook automatically registers on mount and unregisters on unmount,
|
||||
* so no manual cleanup or state management is needed.
|
||||
*/
|
||||
import { useContext, useEffect, useLayoutEffect } from 'react';
|
||||
import instances from '../ink/instances.js';
|
||||
import { AppStoreContext, useAppState } from '../state/AppState.js';
|
||||
import { useContext, useEffect, useLayoutEffect } from 'react'
|
||||
import instances from '../ink/instances.js'
|
||||
import { AppStoreContext, useAppState } from '../state/AppState.js'
|
||||
|
||||
// Non-modal overlays that shouldn't disable TextInput focus
|
||||
const NON_MODAL_OVERLAYS = new Set(['autocomplete']);
|
||||
const NON_MODAL_OVERLAYS = new Set(['autocomplete'])
|
||||
|
||||
/**
|
||||
* Hook to register a component as an active overlay.
|
||||
@@ -35,72 +34,41 @@ const NON_MODAL_OVERLAYS = new Set(['autocomplete']);
|
||||
* // ...
|
||||
* }
|
||||
*/
|
||||
export function useRegisterOverlay(id, t0) {
|
||||
const $ = _c(8);
|
||||
const enabled = t0 === undefined ? true : t0;
|
||||
const store = useContext(AppStoreContext);
|
||||
const setAppState = store?.setState;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== enabled || $[1] !== id || $[2] !== setAppState) {
|
||||
t1 = () => {
|
||||
if (!enabled || !setAppState) {
|
||||
return;
|
||||
}
|
||||
export function useRegisterOverlay(id: string, enabled = true): void {
|
||||
// Use context directly so this is a no-op when rendered outside AppStateProvider
|
||||
// (e.g., in isolated component tests that don't need the full app state tree).
|
||||
const store = useContext(AppStoreContext)
|
||||
const setAppState = store?.setState
|
||||
useEffect(() => {
|
||||
if (!enabled || !setAppState) return
|
||||
setAppState(prev => {
|
||||
if (prev.activeOverlays.has(id)) return prev
|
||||
const next = new Set(prev.activeOverlays)
|
||||
next.add(id)
|
||||
return { ...prev, activeOverlays: next }
|
||||
})
|
||||
return () => {
|
||||
setAppState(prev => {
|
||||
if (prev.activeOverlays.has(id)) {
|
||||
return prev;
|
||||
}
|
||||
const next = new Set(prev.activeOverlays);
|
||||
next.add(id);
|
||||
return {
|
||||
...prev,
|
||||
activeOverlays: next
|
||||
};
|
||||
});
|
||||
return () => {
|
||||
setAppState(prev_0 => {
|
||||
if (!prev_0.activeOverlays.has(id)) {
|
||||
return prev_0;
|
||||
}
|
||||
const next_0 = new Set(prev_0.activeOverlays);
|
||||
next_0.delete(id);
|
||||
return {
|
||||
...prev_0,
|
||||
activeOverlays: next_0
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
t2 = [id, enabled, setAppState];
|
||||
$[0] = enabled;
|
||||
$[1] = id;
|
||||
$[2] = setAppState;
|
||||
$[3] = t1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t2 = $[4];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[5] !== enabled) {
|
||||
t3 = () => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
return _temp;
|
||||
};
|
||||
t4 = [enabled];
|
||||
$[5] = enabled;
|
||||
$[6] = t3;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
t4 = $[7];
|
||||
}
|
||||
useLayoutEffect(t3, t4);
|
||||
if (!prev.activeOverlays.has(id)) return prev
|
||||
const next = new Set(prev.activeOverlays)
|
||||
next.delete(id)
|
||||
return { ...prev, activeOverlays: next }
|
||||
})
|
||||
}
|
||||
}, [id, enabled, setAppState])
|
||||
|
||||
// On overlay close, force the next render to full-damage diff instead
|
||||
// of blit. A tall overlay (e.g. FuzzyPicker with a 20-line preview)
|
||||
// shrinks the Ink-managed region on unmount; the blit fast path can
|
||||
// copy stale cells from the overlay's previous frame into rows the
|
||||
// shorter layout no longer reaches, leaving a ghost title/divider.
|
||||
// useLayoutEffect so cleanup runs synchronously before the microtask-
|
||||
// deferred onRender (scheduleRender queues a microtask from
|
||||
// resetAfterCommit; passive-effect cleanup would land after it).
|
||||
useLayoutEffect(() => {
|
||||
if (!enabled) return
|
||||
return () => instances.get(process.stdout)?.invalidatePrevFrame()
|
||||
}, [enabled])
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,11 +84,8 @@ export function useRegisterOverlay(id, t0) {
|
||||
* useKeybinding('chat:cancel', handleCancel, { isActive })
|
||||
* }
|
||||
*/
|
||||
function _temp() {
|
||||
return instances.get(process.stdout)?.invalidatePrevFrame();
|
||||
}
|
||||
export function useIsOverlayActive() {
|
||||
return useAppState(_temp2);
|
||||
export function useIsOverlayActive(): boolean {
|
||||
return useAppState(s => s.activeOverlays.size > 0)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,17 +99,11 @@ export function useIsOverlayActive() {
|
||||
* // Use for TextInput focus - allows typing during autocomplete
|
||||
* focus: !isSearchingHistory && !isModalOverlayActive
|
||||
*/
|
||||
function _temp2(s) {
|
||||
return s.activeOverlays.size > 0;
|
||||
}
|
||||
export function useIsModalOverlayActive() {
|
||||
return useAppState(_temp3);
|
||||
}
|
||||
function _temp3(s) {
|
||||
for (const id of s.activeOverlays) {
|
||||
if (!NON_MODAL_OVERLAYS.has(id)) {
|
||||
return true;
|
||||
export function useIsModalOverlayActive(): boolean {
|
||||
return useAppState(s => {
|
||||
for (const id of s.activeOverlays) {
|
||||
if (!NON_MODAL_OVERLAYS.has(id)) return true
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
/**
|
||||
* Portal for content that floats above the prompt so it escapes
|
||||
* FullscreenLayout's bottom-slot `overflowY:hidden` clip.
|
||||
@@ -19,106 +18,78 @@ import { c as _c } from "react/compiler-runtime";
|
||||
* Split into data/setter context pairs so writers never re-render on
|
||||
* their own writes — the setter contexts are stable.
|
||||
*/
|
||||
import React, { createContext, type ReactNode, useContext, useEffect, useState } from 'react';
|
||||
import type { SuggestionItem } from '../components/PromptInput/PromptInputFooterSuggestions.js';
|
||||
import React, {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { SuggestionItem } from '../components/PromptInput/PromptInputFooterSuggestions.js'
|
||||
|
||||
export type PromptOverlayData = {
|
||||
suggestions: SuggestionItem[];
|
||||
selectedSuggestion: number;
|
||||
maxColumnWidth?: number;
|
||||
};
|
||||
type Setter<T> = (d: T | null) => void;
|
||||
const DataContext = createContext<PromptOverlayData | null>(null);
|
||||
const SetContext = createContext<Setter<PromptOverlayData> | null>(null);
|
||||
const DialogContext = createContext<ReactNode>(null);
|
||||
const SetDialogContext = createContext<Setter<ReactNode> | null>(null);
|
||||
export function PromptOverlayProvider(t0) {
|
||||
const $ = _c(6);
|
||||
const {
|
||||
children
|
||||
} = t0;
|
||||
const [data, setData] = useState(null);
|
||||
const [dialog, setDialog] = useState(null);
|
||||
let t1;
|
||||
if ($[0] !== children || $[1] !== dialog) {
|
||||
t1 = <DialogContext.Provider value={dialog}>{children}</DialogContext.Provider>;
|
||||
$[0] = children;
|
||||
$[1] = dialog;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== data || $[4] !== t1) {
|
||||
t2 = <SetContext.Provider value={setData}><SetDialogContext.Provider value={setDialog}><DataContext.Provider value={data}>{t1}</DataContext.Provider></SetDialogContext.Provider></SetContext.Provider>;
|
||||
$[3] = data;
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
return t2;
|
||||
suggestions: SuggestionItem[]
|
||||
selectedSuggestion: number
|
||||
maxColumnWidth?: number
|
||||
}
|
||||
export function usePromptOverlay() {
|
||||
return useContext(DataContext);
|
||||
|
||||
type Setter<T> = (d: T | null) => void
|
||||
|
||||
const DataContext = createContext<PromptOverlayData | null>(null)
|
||||
const SetContext = createContext<Setter<PromptOverlayData> | null>(null)
|
||||
const DialogContext = createContext<ReactNode>(null)
|
||||
const SetDialogContext = createContext<Setter<ReactNode> | null>(null)
|
||||
|
||||
export function PromptOverlayProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}): ReactNode {
|
||||
const [data, setData] = useState<PromptOverlayData | null>(null)
|
||||
const [dialog, setDialog] = useState<ReactNode>(null)
|
||||
return (
|
||||
<SetContext.Provider value={setData}>
|
||||
<SetDialogContext.Provider value={setDialog}>
|
||||
<DataContext.Provider value={data}>
|
||||
<DialogContext.Provider value={dialog}>
|
||||
{children}
|
||||
</DialogContext.Provider>
|
||||
</DataContext.Provider>
|
||||
</SetDialogContext.Provider>
|
||||
</SetContext.Provider>
|
||||
)
|
||||
}
|
||||
export function usePromptOverlayDialog() {
|
||||
return useContext(DialogContext);
|
||||
|
||||
export function usePromptOverlay(): PromptOverlayData | null {
|
||||
return useContext(DataContext)
|
||||
}
|
||||
|
||||
export function usePromptOverlayDialog(): ReactNode {
|
||||
return useContext(DialogContext)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register suggestion data for the floating overlay. Clears on unmount.
|
||||
* No-op outside the provider (non-fullscreen renders inline instead).
|
||||
*/
|
||||
export function useSetPromptOverlay(data) {
|
||||
const $ = _c(4);
|
||||
const set = useContext(SetContext);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== data || $[1] !== set) {
|
||||
t0 = () => {
|
||||
if (!set) {
|
||||
return;
|
||||
}
|
||||
set(data);
|
||||
return () => set(null);
|
||||
};
|
||||
t1 = [set, data];
|
||||
$[0] = data;
|
||||
$[1] = set;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
t1 = $[3];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
export function useSetPromptOverlay(data: PromptOverlayData | null): void {
|
||||
const set = useContext(SetContext)
|
||||
useEffect(() => {
|
||||
if (!set) return
|
||||
set(data)
|
||||
return () => set(null)
|
||||
}, [set, data])
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a dialog node to float above the prompt. Clears on unmount.
|
||||
* No-op outside the provider (non-fullscreen renders inline instead).
|
||||
*/
|
||||
export function useSetPromptOverlayDialog(node) {
|
||||
const $ = _c(4);
|
||||
const set = useContext(SetDialogContext);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== node || $[1] !== set) {
|
||||
t0 = () => {
|
||||
if (!set) {
|
||||
return;
|
||||
}
|
||||
set(node);
|
||||
return () => set(null);
|
||||
};
|
||||
t1 = [set, node];
|
||||
$[0] = node;
|
||||
$[1] = set;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
t1 = $[3];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
export function useSetPromptOverlayDialog(node: ReactNode): void {
|
||||
const set = useContext(SetDialogContext)
|
||||
useEffect(() => {
|
||||
if (!set) return
|
||||
set(node)
|
||||
return () => set(null)
|
||||
}, [set, node])
|
||||
}
|
||||
|
||||
@@ -1,219 +1,173 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo } from 'react';
|
||||
import { saveCurrentProjectConfig } from '../utils/config.js';
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { saveCurrentProjectConfig } from '../utils/config.js'
|
||||
|
||||
export type StatsStore = {
|
||||
increment(name: string, value?: number): void;
|
||||
set(name: string, value: number): void;
|
||||
observe(name: string, value: number): void;
|
||||
add(name: string, value: string): void;
|
||||
getAll(): Record<string, number>;
|
||||
};
|
||||
function percentile(sorted: number[], p: number): number {
|
||||
const index = p / 100 * (sorted.length - 1);
|
||||
const lower = Math.floor(index);
|
||||
const upper = Math.ceil(index);
|
||||
if (lower === upper) {
|
||||
return sorted[lower]!;
|
||||
}
|
||||
return sorted[lower]! + (sorted[upper]! - sorted[lower]!) * (index - lower);
|
||||
increment(name: string, value?: number): void
|
||||
set(name: string, value: number): void
|
||||
observe(name: string, value: number): void
|
||||
add(name: string, value: string): void
|
||||
getAll(): Record<string, number>
|
||||
}
|
||||
const RESERVOIR_SIZE = 1024;
|
||||
|
||||
function percentile(sorted: number[], p: number): number {
|
||||
const index = (p / 100) * (sorted.length - 1)
|
||||
const lower = Math.floor(index)
|
||||
const upper = Math.ceil(index)
|
||||
if (lower === upper) {
|
||||
return sorted[lower]!
|
||||
}
|
||||
return sorted[lower]! + (sorted[upper]! - sorted[lower]!) * (index - lower)
|
||||
}
|
||||
|
||||
const RESERVOIR_SIZE = 1024
|
||||
|
||||
type Histogram = {
|
||||
reservoir: number[];
|
||||
count: number;
|
||||
sum: number;
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
reservoir: number[]
|
||||
count: number
|
||||
sum: number
|
||||
min: number
|
||||
max: number
|
||||
}
|
||||
|
||||
export function createStatsStore(): StatsStore {
|
||||
const metrics = new Map<string, number>();
|
||||
const histograms = new Map<string, Histogram>();
|
||||
const sets = new Map<string, Set<string>>();
|
||||
const metrics = new Map<string, number>()
|
||||
const histograms = new Map<string, Histogram>()
|
||||
const sets = new Map<string, Set<string>>()
|
||||
|
||||
return {
|
||||
increment(name: string, value = 1) {
|
||||
metrics.set(name, (metrics.get(name) ?? 0) + value);
|
||||
metrics.set(name, (metrics.get(name) ?? 0) + value)
|
||||
},
|
||||
set(name: string, value: number) {
|
||||
metrics.set(name, value);
|
||||
metrics.set(name, value)
|
||||
},
|
||||
observe(name: string, value: number) {
|
||||
let h = histograms.get(name);
|
||||
let h = histograms.get(name)
|
||||
if (!h) {
|
||||
h = {
|
||||
reservoir: [],
|
||||
count: 0,
|
||||
sum: 0,
|
||||
min: value,
|
||||
max: value
|
||||
};
|
||||
histograms.set(name, h);
|
||||
h = { reservoir: [], count: 0, sum: 0, min: value, max: value }
|
||||
histograms.set(name, h)
|
||||
}
|
||||
h.count++;
|
||||
h.sum += value;
|
||||
h.count++
|
||||
h.sum += value
|
||||
if (value < h.min) {
|
||||
h.min = value;
|
||||
h.min = value
|
||||
}
|
||||
if (value > h.max) {
|
||||
h.max = value;
|
||||
h.max = value
|
||||
}
|
||||
// Reservoir sampling (Algorithm R)
|
||||
if (h.reservoir.length < RESERVOIR_SIZE) {
|
||||
h.reservoir.push(value);
|
||||
h.reservoir.push(value)
|
||||
} else {
|
||||
const j = Math.floor(Math.random() * h.count);
|
||||
const j = Math.floor(Math.random() * h.count)
|
||||
if (j < RESERVOIR_SIZE) {
|
||||
h.reservoir[j] = value;
|
||||
h.reservoir[j] = value
|
||||
}
|
||||
}
|
||||
},
|
||||
add(name: string, value: string) {
|
||||
let s = sets.get(name);
|
||||
let s = sets.get(name)
|
||||
if (!s) {
|
||||
s = new Set();
|
||||
sets.set(name, s);
|
||||
s = new Set()
|
||||
sets.set(name, s)
|
||||
}
|
||||
s.add(value);
|
||||
s.add(value)
|
||||
},
|
||||
getAll() {
|
||||
const result: Record<string, number> = Object.fromEntries(metrics);
|
||||
const result: Record<string, number> = Object.fromEntries(metrics)
|
||||
|
||||
for (const [name, h] of histograms) {
|
||||
if (h.count === 0) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
result[`${name}_count`] = h.count;
|
||||
result[`${name}_min`] = h.min;
|
||||
result[`${name}_max`] = h.max;
|
||||
result[`${name}_avg`] = h.sum / h.count;
|
||||
const sorted = [...h.reservoir].sort((a, b) => a - b);
|
||||
result[`${name}_p50`] = percentile(sorted, 50);
|
||||
result[`${name}_p95`] = percentile(sorted, 95);
|
||||
result[`${name}_p99`] = percentile(sorted, 99);
|
||||
result[`${name}_count`] = h.count
|
||||
result[`${name}_min`] = h.min
|
||||
result[`${name}_max`] = h.max
|
||||
result[`${name}_avg`] = h.sum / h.count
|
||||
const sorted = [...h.reservoir].sort((a, b) => a - b)
|
||||
result[`${name}_p50`] = percentile(sorted, 50)
|
||||
result[`${name}_p95`] = percentile(sorted, 95)
|
||||
result[`${name}_p99`] = percentile(sorted, 99)
|
||||
}
|
||||
|
||||
for (const [name, s] of sets) {
|
||||
result[name] = s.size;
|
||||
result[name] = s.size
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
return result
|
||||
},
|
||||
}
|
||||
}
|
||||
export const StatsContext = createContext<StatsStore | null>(null);
|
||||
|
||||
export const StatsContext = createContext<StatsStore | null>(null)
|
||||
|
||||
type Props = {
|
||||
store?: StatsStore;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
export function StatsProvider(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
store: externalStore,
|
||||
children
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = createStatsStore();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const internalStore = t1;
|
||||
const store = externalStore ?? internalStore;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[1] !== store) {
|
||||
t2 = () => {
|
||||
const flush = () => {
|
||||
const metrics = store.getAll();
|
||||
if (Object.keys(metrics).length > 0) {
|
||||
saveCurrentProjectConfig(current => ({
|
||||
...current,
|
||||
lastSessionMetrics: metrics
|
||||
}));
|
||||
}
|
||||
};
|
||||
process.on("exit", flush);
|
||||
return () => {
|
||||
process.off("exit", flush);
|
||||
};
|
||||
};
|
||||
t3 = [store];
|
||||
$[1] = store;
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
t3 = $[3];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[4] !== children || $[5] !== store) {
|
||||
t4 = <StatsContext.Provider value={store}>{children}</StatsContext.Provider>;
|
||||
$[4] = children;
|
||||
$[5] = store;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
return t4;
|
||||
store?: StatsStore
|
||||
children: React.ReactNode
|
||||
}
|
||||
export function useStats() {
|
||||
const store = useContext(StatsContext);
|
||||
|
||||
export function StatsProvider({
|
||||
store: externalStore,
|
||||
children,
|
||||
}: Props): React.ReactNode {
|
||||
const internalStore = useMemo(() => createStatsStore(), [])
|
||||
const store = externalStore ?? internalStore
|
||||
|
||||
useEffect(() => {
|
||||
const flush = () => {
|
||||
const metrics = store.getAll()
|
||||
if (Object.keys(metrics).length > 0) {
|
||||
saveCurrentProjectConfig(current => ({
|
||||
...current,
|
||||
lastSessionMetrics: metrics,
|
||||
}))
|
||||
}
|
||||
}
|
||||
process.on('exit', flush)
|
||||
return () => {
|
||||
process.off('exit', flush)
|
||||
}
|
||||
}, [store])
|
||||
|
||||
return <StatsContext.Provider value={store}>{children}</StatsContext.Provider>
|
||||
}
|
||||
|
||||
export function useStats(): StatsStore {
|
||||
const store = useContext(StatsContext)
|
||||
if (!store) {
|
||||
throw new Error("useStats must be used within a StatsProvider");
|
||||
throw new Error('useStats must be used within a StatsProvider')
|
||||
}
|
||||
return store;
|
||||
return store
|
||||
}
|
||||
export function useCounter(name) {
|
||||
const $ = _c(3);
|
||||
const store = useStats();
|
||||
let t0;
|
||||
if ($[0] !== name || $[1] !== store) {
|
||||
t0 = value => store.increment(name, value);
|
||||
$[0] = name;
|
||||
$[1] = store;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
return t0;
|
||||
|
||||
export function useCounter(name: string): (value?: number) => void {
|
||||
const store = useStats()
|
||||
return useCallback(
|
||||
(value?: number) => store.increment(name, value),
|
||||
[store, name],
|
||||
)
|
||||
}
|
||||
export function useGauge(name) {
|
||||
const $ = _c(3);
|
||||
const store = useStats();
|
||||
let t0;
|
||||
if ($[0] !== name || $[1] !== store) {
|
||||
t0 = value => store.set(name, value);
|
||||
$[0] = name;
|
||||
$[1] = store;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
return t0;
|
||||
|
||||
export function useGauge(name: string): (value: number) => void {
|
||||
const store = useStats()
|
||||
return useCallback((value: number) => store.set(name, value), [store, name])
|
||||
}
|
||||
export function useTimer(name) {
|
||||
const $ = _c(3);
|
||||
const store = useStats();
|
||||
let t0;
|
||||
if ($[0] !== name || $[1] !== store) {
|
||||
t0 = value => store.observe(name, value);
|
||||
$[0] = name;
|
||||
$[1] = store;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
return t0;
|
||||
|
||||
export function useTimer(name: string): (value: number) => void {
|
||||
const store = useStats()
|
||||
return useCallback(
|
||||
(value: number) => store.observe(name, value),
|
||||
[store, name],
|
||||
)
|
||||
}
|
||||
export function useSet(name) {
|
||||
const $ = _c(3);
|
||||
const store = useStats();
|
||||
let t0;
|
||||
if ($[0] !== name || $[1] !== store) {
|
||||
t0 = value => store.add(name, value);
|
||||
$[0] = name;
|
||||
$[1] = store;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
return t0;
|
||||
|
||||
export function useSet(name: string): (value: string) => void {
|
||||
const store = useStats()
|
||||
return useCallback((value: string) => store.add(name, value), [store, name])
|
||||
}
|
||||
|
||||
@@ -1,71 +1,58 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { createContext, useContext, useState, useSyncExternalStore } from 'react';
|
||||
import { createStore, type Store } from '../state/store.js';
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from 'react'
|
||||
import { createStore, type Store } from '../state/store.js'
|
||||
|
||||
export type VoiceState = {
|
||||
voiceState: 'idle' | 'recording' | 'processing';
|
||||
voiceError: string | null;
|
||||
voiceInterimTranscript: string;
|
||||
voiceAudioLevels: number[];
|
||||
voiceWarmingUp: boolean;
|
||||
};
|
||||
voiceState: 'idle' | 'recording' | 'processing'
|
||||
voiceError: string | null
|
||||
voiceInterimTranscript: string
|
||||
voiceAudioLevels: number[]
|
||||
voiceWarmingUp: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: VoiceState = {
|
||||
voiceState: 'idle',
|
||||
voiceError: null,
|
||||
voiceInterimTranscript: '',
|
||||
voiceAudioLevels: [],
|
||||
voiceWarmingUp: false
|
||||
};
|
||||
type VoiceStore = Store<VoiceState>;
|
||||
const VoiceContext = createContext<VoiceStore | null>(null);
|
||||
voiceWarmingUp: false,
|
||||
}
|
||||
|
||||
type VoiceStore = Store<VoiceState>
|
||||
|
||||
const VoiceContext = createContext<VoiceStore | null>(null)
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
export function VoiceProvider(t0) {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
children
|
||||
} = t0;
|
||||
const [store] = useState(_temp);
|
||||
let t1;
|
||||
if ($[0] !== children || $[1] !== store) {
|
||||
t1 = <VoiceContext.Provider value={store}>{children}</VoiceContext.Provider>;
|
||||
$[0] = children;
|
||||
$[1] = store;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
children: React.ReactNode
|
||||
}
|
||||
function _temp() {
|
||||
return createStore(DEFAULT_STATE);
|
||||
|
||||
export function VoiceProvider({ children }: Props): React.ReactNode {
|
||||
// Store is created once — stable context value means the provider never
|
||||
// triggers re-renders. Consumers subscribe to slices via useVoiceState.
|
||||
const [store] = useState(() => createStore<VoiceState>(DEFAULT_STATE))
|
||||
return <VoiceContext.Provider value={store}>{children}</VoiceContext.Provider>
|
||||
}
|
||||
function useVoiceStore() {
|
||||
const store = useContext(VoiceContext);
|
||||
|
||||
function useVoiceStore(): VoiceStore {
|
||||
const store = useContext(VoiceContext)
|
||||
if (!store) {
|
||||
throw new Error("useVoiceState must be used within a VoiceProvider");
|
||||
throw new Error('useVoiceState must be used within a VoiceProvider')
|
||||
}
|
||||
return store;
|
||||
return store
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a slice of voice state. Only re-renders when the selected
|
||||
* value changes (compared via Object.is).
|
||||
*/
|
||||
export function useVoiceState(selector) {
|
||||
const $ = _c(3);
|
||||
const store = useVoiceStore();
|
||||
let t0;
|
||||
if ($[0] !== selector || $[1] !== store) {
|
||||
t0 = () => selector(store.getState());
|
||||
$[0] = selector;
|
||||
$[1] = store;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
const get = t0;
|
||||
return useSyncExternalStore(store.subscribe, get, get);
|
||||
export function useVoiceState<T>(selector: (state: VoiceState) => T): T {
|
||||
const store = useVoiceStore()
|
||||
const get = () => selector(store.getState())
|
||||
return useSyncExternalStore(store.subscribe, get, get)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,8 +60,10 @@ export function useVoiceState(selector) {
|
||||
* store.setState is synchronous: callers can read getVoiceState() immediately
|
||||
* after to observe the new value (VoiceKeybindingHandler relies on this).
|
||||
*/
|
||||
export function useSetVoiceState() {
|
||||
return useVoiceStore().setState;
|
||||
export function useSetVoiceState(): (
|
||||
updater: (prev: VoiceState) => VoiceState,
|
||||
) => void {
|
||||
return useVoiceStore().setState
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,6 +71,6 @@ export function useSetVoiceState() {
|
||||
* useVoiceState (which subscribes), this doesn't cause re-renders — use
|
||||
* inside event handlers that need to read state set earlier in the same tick.
|
||||
*/
|
||||
export function useGetVoiceState() {
|
||||
return useVoiceStore().getState;
|
||||
export function useGetVoiceState(): () => VoiceState {
|
||||
return useVoiceStore().getState
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user