Initial public release
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
export const calculateContextUsage = (
|
||||
totalTokens: number,
|
||||
contextLimit: number,
|
||||
outputLimit: number
|
||||
) => {
|
||||
const safeContext = Number.isFinite(contextLimit) ? Math.max(contextLimit, 0) : 0;
|
||||
const hasOutputLimit = Number.isFinite(outputLimit) && outputLimit > 0;
|
||||
const safeOutput = hasOutputLimit ? Math.max(outputLimit, 0) : 0;
|
||||
|
||||
const effectiveOutputReservation = Math.min(hasOutputLimit ? safeOutput : 32000, 32000);
|
||||
const normalizedOutput = Math.min(effectiveOutputReservation, safeContext);
|
||||
const thresholdLimit = safeContext > 0 ? Math.max(safeContext - normalizedOutput, 1) : 0;
|
||||
const percentage = thresholdLimit > 0 ? (totalTokens / thresholdLimit) * 100 : 0;
|
||||
|
||||
return {
|
||||
percentage: Math.min(percentage, 100),
|
||||
contextLimit: safeContext,
|
||||
outputLimit: safeOutput,
|
||||
thresholdLimit: thresholdLimit || 1,
|
||||
normalizedOutput
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { Part } from "@opencode-ai/sdk";
|
||||
|
||||
const extractTextFromDelta = (delta: unknown): string => {
|
||||
if (!delta) return '';
|
||||
if (typeof delta === 'string') return delta;
|
||||
if (Array.isArray(delta)) {
|
||||
return delta.map((item) => extractTextFromDelta(item)).join('');
|
||||
}
|
||||
if (typeof delta === 'object') {
|
||||
if (typeof (delta as { text?: unknown }).text === 'string') {
|
||||
return (delta as { text: string }).text;
|
||||
}
|
||||
if (Array.isArray((delta as { content?: unknown[] }).content)) {
|
||||
return (delta as { content: unknown[] }).content.map((item: unknown) => extractTextFromDelta(item)).join('');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const extractTextFromPart = (part: unknown): string => {
|
||||
if (!part) return '';
|
||||
const typedPart = part as { text?: string | unknown[]; delta?: unknown; content?: string | unknown[] };
|
||||
if (typeof typedPart.text === 'string') return typedPart.text;
|
||||
if (Array.isArray(typedPart.text)) {
|
||||
return typedPart.text.map((item: unknown) => (typeof item === 'string' ? item : extractTextFromPart(item))).join('');
|
||||
}
|
||||
const deltaText = extractTextFromDelta(typedPart.delta);
|
||||
if (deltaText) return deltaText;
|
||||
if (typeof typedPart.content === 'string') return typedPart.content;
|
||||
if (Array.isArray(typedPart.content)) {
|
||||
return typedPart.content
|
||||
.map((item: unknown) => {
|
||||
if (typeof item === 'string') return item;
|
||||
if (item && typeof item === 'object') {
|
||||
const typedItem = item as { text?: string; delta?: unknown };
|
||||
return typedItem.text || extractTextFromDelta(typedItem.delta) || '';
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const normalizeStreamingPart = (incoming: Part, existing?: Part): Part => {
|
||||
const normalized: { type?: string; text?: string; delta?: unknown; [key: string]: unknown } = { ...incoming } as { type?: string; text?: string; delta?: unknown; [key: string]: unknown };
|
||||
normalized.type = normalized.type || 'text';
|
||||
|
||||
if (normalized.type === 'text') {
|
||||
const existingText = existing && typeof (existing as { text?: string }).text === 'string' ? (existing as { text: string }).text : '';
|
||||
const directText = typeof normalized.text === 'string' ? normalized.text : '';
|
||||
const deltaText = extractTextFromDelta((incoming as { delta?: unknown }).delta);
|
||||
|
||||
if (directText) {
|
||||
normalized.text = directText;
|
||||
} else if (deltaText) {
|
||||
normalized.text = existingText ? `${existingText}${deltaText}` : deltaText;
|
||||
} else if (existingText) {
|
||||
normalized.text = existingText;
|
||||
} else {
|
||||
normalized.text = '';
|
||||
}
|
||||
|
||||
delete normalized.delta;
|
||||
}
|
||||
|
||||
return normalized as Part;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { EditPermissionMode } from "../types/sessionTypes";
|
||||
|
||||
const EDIT_PERMISSION_TOOL_NAMES = new Set([
|
||||
'edit',
|
||||
'multiedit',
|
||||
'str_replace',
|
||||
'str_replace_based_edit_tool',
|
||||
'write',
|
||||
]);
|
||||
|
||||
export const isEditPermissionType = (type?: string | null): boolean => {
|
||||
if (!type) {
|
||||
return false;
|
||||
}
|
||||
return EDIT_PERMISSION_TOOL_NAMES.has(type.toLowerCase());
|
||||
};
|
||||
|
||||
const resolveConfigStore = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
return (window as { __zustand_config_store__?: { getState?: () => { agents?: Array<{ name: string; permission?: { edit?: string }; tools?: { edit?: boolean } }> } } }).__zustand_config_store__;
|
||||
};
|
||||
|
||||
const getAgentDefinition = (agentName?: string): { name: string; permission?: { edit?: string }; tools?: { edit?: boolean } } | undefined => {
|
||||
if (!agentName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const configStore = resolveConfigStore();
|
||||
if (configStore?.getState) {
|
||||
const state = configStore.getState();
|
||||
return state.agents?.find?.((agent: { name: string; permission?: { edit?: string }; tools?: { edit?: boolean } }) => agent.name === agentName);
|
||||
}
|
||||
} catch { /* ignored */ }
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getAgentDefaultEditPermission = (agentName?: string): EditPermissionMode => {
|
||||
const agent = getAgentDefinition(agentName);
|
||||
if (!agent) {
|
||||
return 'ask';
|
||||
}
|
||||
|
||||
const permission = agent.permission?.edit;
|
||||
if (permission === 'allow' || permission === 'ask' || permission === 'deny' || permission === 'full') {
|
||||
return permission;
|
||||
}
|
||||
|
||||
const editToolEnabled = agent.tools ? agent.tools.edit !== false : true;
|
||||
return editToolEnabled ? 'ask' : 'deny';
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
let safeStorageInstance: Storage | null = null;
|
||||
|
||||
const createInMemoryStorage = (): Storage => {
|
||||
const store = new Map<string, string>();
|
||||
return {
|
||||
getItem: (key: string) => store.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store.set(key, value);
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
store.delete(key);
|
||||
},
|
||||
clear: () => {
|
||||
store.clear();
|
||||
},
|
||||
key: (index: number) => Array.from(store.keys())[index] ?? null,
|
||||
get length() {
|
||||
return store.size;
|
||||
},
|
||||
} as Storage;
|
||||
};
|
||||
|
||||
const createSafeStorage = (): Storage => {
|
||||
if (typeof window === 'undefined' || !window.localStorage) {
|
||||
return createInMemoryStorage();
|
||||
}
|
||||
|
||||
const baseStorage = window.localStorage;
|
||||
const fallback = createInMemoryStorage();
|
||||
let storageAvailable = true;
|
||||
|
||||
const disableStorage = () => {
|
||||
storageAvailable = false;
|
||||
};
|
||||
|
||||
const safeGet = (key: string): string | null => {
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
const value = baseStorage.getItem(key);
|
||||
if (value !== null) {
|
||||
return value;
|
||||
}
|
||||
} catch {
|
||||
disableStorage();
|
||||
}
|
||||
}
|
||||
return fallback.getItem(key);
|
||||
};
|
||||
|
||||
const safeSet = (key: string, value: string) => {
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
baseStorage.setItem(key, value);
|
||||
fallback.removeItem(key);
|
||||
return;
|
||||
} catch {
|
||||
disableStorage();
|
||||
}
|
||||
}
|
||||
fallback.setItem(key, value);
|
||||
};
|
||||
|
||||
const safeRemove = (key: string) => {
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
baseStorage.removeItem(key);
|
||||
} catch {
|
||||
disableStorage();
|
||||
}
|
||||
}
|
||||
fallback.removeItem(key);
|
||||
};
|
||||
|
||||
const safeClear = () => {
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
baseStorage.clear();
|
||||
} catch {
|
||||
disableStorage();
|
||||
}
|
||||
}
|
||||
fallback.clear();
|
||||
};
|
||||
|
||||
const safeKey = (index: number): string | null => {
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
return baseStorage.key(index);
|
||||
} catch {
|
||||
disableStorage();
|
||||
}
|
||||
}
|
||||
return fallback.key(index);
|
||||
};
|
||||
|
||||
return {
|
||||
getItem: safeGet,
|
||||
setItem: safeSet,
|
||||
removeItem: safeRemove,
|
||||
clear: safeClear,
|
||||
key: safeKey,
|
||||
get length() {
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
return baseStorage.length + fallback.length;
|
||||
} catch {
|
||||
disableStorage();
|
||||
}
|
||||
}
|
||||
return fallback.length;
|
||||
},
|
||||
} as Storage;
|
||||
};
|
||||
|
||||
export const getSafeStorage = (): Storage => {
|
||||
if (!safeStorageInstance) {
|
||||
safeStorageInstance = createSafeStorage();
|
||||
}
|
||||
return safeStorageInstance;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const streamDebugEnabled = (): boolean => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
try {
|
||||
return window.localStorage.getItem('openchamber_stream_debug') === '1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { MessageStreamLifecycle } from "../types/sessionTypes";
|
||||
|
||||
export type { MessageStreamLifecycle };
|
||||
|
||||
export const touchStreamingLifecycle = (
|
||||
source: Map<string, MessageStreamLifecycle>,
|
||||
messageId: string
|
||||
): Map<string, MessageStreamLifecycle> => {
|
||||
const now = Date.now();
|
||||
const existing = source.get(messageId);
|
||||
|
||||
const next = new Map(source);
|
||||
next.set(messageId, {
|
||||
phase: 'streaming',
|
||||
startedAt: existing?.startedAt ?? now,
|
||||
lastUpdateAt: now,
|
||||
});
|
||||
|
||||
return next;
|
||||
};
|
||||
|
||||
|
||||
export const removeLifecycleEntries = (
|
||||
source: Map<string, MessageStreamLifecycle>,
|
||||
ids: Iterable<string>
|
||||
): Map<string, MessageStreamLifecycle> => {
|
||||
const idsArray = Array.from(ids);
|
||||
const shouldClone = idsArray.some((id) => source.has(id));
|
||||
|
||||
if (!shouldClone) {
|
||||
return source;
|
||||
}
|
||||
|
||||
const next = new Map(source);
|
||||
idsArray.forEach((id) => {
|
||||
next.delete(id);
|
||||
});
|
||||
|
||||
return next;
|
||||
};
|
||||
|
||||
const lifecycleCompletionTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
export const clearLifecycleCompletionTimer = (messageId: string) => {
|
||||
const timer = lifecycleCompletionTimers.get(messageId);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
lifecycleCompletionTimers.delete(messageId);
|
||||
}
|
||||
};
|
||||
|
||||
export const clearLifecycleTimersForIds = (ids: Iterable<string>) => {
|
||||
for (const id of ids) {
|
||||
clearLifecycleCompletionTimer(id);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Message, Part } from "@opencode-ai/sdk";
|
||||
|
||||
type TokenBreakdown = {
|
||||
input?: number;
|
||||
output?: number;
|
||||
reasoning?: number;
|
||||
cache?: {
|
||||
read?: number;
|
||||
write?: number;
|
||||
};
|
||||
};
|
||||
|
||||
const sumTokenBreakdown = (breakdown: TokenBreakdown | null | undefined): number => {
|
||||
if (!breakdown || typeof breakdown !== 'object') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const inputTokens = breakdown.input ?? 0;
|
||||
const outputTokens = breakdown.output ?? 0;
|
||||
const reasoningTokens = breakdown.reasoning ?? 0;
|
||||
const cacheReadTokens = breakdown.cache && typeof breakdown.cache === 'object' ? breakdown.cache.read ?? 0 : 0;
|
||||
const cacheWriteTokens = breakdown.cache && typeof breakdown.cache === 'object' ? breakdown.cache.write ?? 0 : 0;
|
||||
|
||||
return inputTokens + outputTokens + reasoningTokens + cacheReadTokens + cacheWriteTokens;
|
||||
};
|
||||
|
||||
export const extractTokensFromMessage = (message: { info: Message; parts: Part[] }): number => {
|
||||
const tokens = (message.info as { tokens?: number | TokenBreakdown }).tokens;
|
||||
|
||||
if (typeof tokens === 'number') {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
if (tokens && typeof tokens === 'object') {
|
||||
return sumTokenBreakdown(tokens);
|
||||
}
|
||||
|
||||
const tokenPart = message.parts.find(
|
||||
(part) => typeof (part as { tokens?: number | TokenBreakdown }).tokens !== 'undefined'
|
||||
) as { tokens?: number | TokenBreakdown } | undefined;
|
||||
|
||||
if (!tokenPart || typeof tokenPart.tokens === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (typeof tokenPart.tokens === 'number') {
|
||||
return tokenPart.tokens;
|
||||
}
|
||||
|
||||
return sumTokenBreakdown(tokenPart.tokens);
|
||||
};
|
||||
Reference in New Issue
Block a user