更新大量 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:
claude-code-best
2026-04-04 23:24:27 +08:00
committed by GitHub
parent 02694918b5
commit 5b1a52b8e0
559 changed files with 103807 additions and 101817 deletions

View File

@@ -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>
)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
)
}

View File

@@ -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
})
}

View File

@@ -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])
}

View File

@@ -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])
}

View File

@@ -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
}