feat(app): add tool call detail levels (#2031)
* feat(app): compact tool call sequences * feat(app): summarize compact tool activity * feat(app): add tool call detail levels * fix(app): stabilize compact tool groups * fix(app): address compact tool review feedback
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query";
|
||||
import { queryClient as appQueryClient } from "@/data/query-client";
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
MIN_UI_FONT_SIZE,
|
||||
loadAppSettingsFromStorage as loadAppSettingsFromStoragePure,
|
||||
loadSettingsFromStorage as loadSettingsFromStoragePure,
|
||||
normalizeAppSettings,
|
||||
parseClampedFontSize,
|
||||
parseTerminalScrollbackLines,
|
||||
sanitizeFontFamily,
|
||||
@@ -129,9 +130,10 @@ export function useAppSettings(): UseAppSettingsReturn {
|
||||
throw err;
|
||||
}
|
||||
}, [queryClient]);
|
||||
const settings = useMemo(() => normalizeAppSettings(data), [data]);
|
||||
|
||||
return {
|
||||
settings: data ?? DEFAULT_CLIENT_SETTINGS,
|
||||
settings,
|
||||
isLoading: isPending,
|
||||
error: error ?? null,
|
||||
updateSettings,
|
||||
@@ -186,6 +188,9 @@ export function useSettings<TSelected>(
|
||||
if (updates.autoExpandReasoning !== undefined) {
|
||||
appUpdates.autoExpandReasoning = updates.autoExpandReasoning;
|
||||
}
|
||||
if (updates.toolCallDetailLevel !== undefined) {
|
||||
appUpdates.toolCallDetailLevel = updates.toolCallDetailLevel;
|
||||
}
|
||||
const promises: Promise<void>[] = [];
|
||||
if (Object.keys(appUpdates).length > 0) {
|
||||
promises.push(appSettings.updateSettings(appUpdates));
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
APP_SETTINGS_KEY,
|
||||
APP_SETTINGS_QUERY_KEY,
|
||||
DEFAULT_APP_SETTINGS,
|
||||
DEFAULT_CLIENT_SETTINGS,
|
||||
DEFAULT_CODE_FONT_SIZE,
|
||||
@@ -282,6 +283,27 @@ describe("saveAppSettings", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes a legacy cached settings shape before saving", async () => {
|
||||
const deps = makeDeps();
|
||||
const queryClient = new QueryClient();
|
||||
queryClient.setQueryData(APP_SETTINGS_QUERY_KEY, {
|
||||
theme: "dark",
|
||||
compactToolCalls: true,
|
||||
});
|
||||
|
||||
await saveAppSettings({
|
||||
queryClient,
|
||||
updates: { theme: "light" },
|
||||
deps,
|
||||
});
|
||||
|
||||
expect(JSON.parse(deps.storage.entries.get(APP_SETTINGS_KEY) ?? "null")).toEqual({
|
||||
...DEFAULT_CLIENT_SETTINGS,
|
||||
theme: "light",
|
||||
toolCallDetailLevel: "overview",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseTerminalScrollbackLines", () => {
|
||||
@@ -306,6 +328,27 @@ describe("appearance settings", () => {
|
||||
expect(result.uiFontSize).toBe(DEFAULT_UI_FONT_SIZE);
|
||||
expect(result.codeFontSize).toBe(DEFAULT_CODE_FONT_SIZE);
|
||||
expect(result.syntaxTheme).toBe("one");
|
||||
expect(result.toolCallDetailLevel).toBe("detailed");
|
||||
});
|
||||
|
||||
it("migrates the enabled compact tool call preference to overview", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
[APP_SETTINGS_KEY]: JSON.stringify({ compactToolCalls: true }),
|
||||
}),
|
||||
});
|
||||
|
||||
expect((await loadAppSettingsFromStorage(deps)).toolCallDetailLevel).toBe("overview");
|
||||
});
|
||||
|
||||
it("loads an explicit tool call detail level", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
[APP_SETTINGS_KEY]: JSON.stringify({ toolCallDetailLevel: "concise" }),
|
||||
}),
|
||||
});
|
||||
|
||||
expect((await loadAppSettingsFromStorage(deps)).toolCallDetailLevel).toBe("concise");
|
||||
});
|
||||
|
||||
it("clamps the UI font size into range and rejects non-numeric values", async () => {
|
||||
|
||||
@@ -12,10 +12,16 @@ export type SendBehavior = "interrupt" | "queue";
|
||||
export type ReleaseChannel = "stable" | "beta";
|
||||
export type ServiceUrlBehavior = "ask" | "in-app" | "external";
|
||||
export type WorkspaceTitleSource = "title" | "branch";
|
||||
export type ToolCallDetailLevel = "overview" | "concise" | "detailed";
|
||||
|
||||
const VALID_THEMES = new Set<string>([...Object.keys(THEME_TO_UNISTYLES), "auto"]);
|
||||
const VALID_SERVICE_URL_BEHAVIORS = new Set<ServiceUrlBehavior>(["ask", "in-app", "external"]);
|
||||
const VALID_WORKSPACE_TITLE_SOURCES = new Set<WorkspaceTitleSource>(["title", "branch"]);
|
||||
const VALID_TOOL_CALL_DETAIL_LEVELS = new Set<ToolCallDetailLevel>([
|
||||
"overview",
|
||||
"concise",
|
||||
"detailed",
|
||||
]);
|
||||
export const DEFAULT_TERMINAL_SCROLLBACK_LINES = 10_000;
|
||||
export const MIN_TERMINAL_SCROLLBACK_LINES = 0;
|
||||
export const MAX_TERMINAL_SCROLLBACK_LINES = 1_000_000;
|
||||
@@ -40,6 +46,7 @@ export interface AppSettings {
|
||||
syntaxTheme: SyntaxThemeId; // default "one"
|
||||
workspaceTitleSource: WorkspaceTitleSource;
|
||||
autoExpandReasoning: boolean;
|
||||
toolCallDetailLevel: ToolCallDetailLevel;
|
||||
}
|
||||
|
||||
export interface Settings extends AppSettings {
|
||||
@@ -47,6 +54,8 @@ export interface Settings extends AppSettings {
|
||||
releaseChannel: ReleaseChannel;
|
||||
}
|
||||
|
||||
type StoredAppSettings = Partial<AppSettings> & { compactToolCalls?: unknown };
|
||||
|
||||
export const DEFAULT_CLIENT_SETTINGS: AppSettings = {
|
||||
theme: "auto",
|
||||
language: "system",
|
||||
@@ -60,6 +69,7 @@ export const DEFAULT_CLIENT_SETTINGS: AppSettings = {
|
||||
syntaxTheme: "one",
|
||||
workspaceTitleSource: "title",
|
||||
autoExpandReasoning: false,
|
||||
toolCallDetailLevel: "detailed",
|
||||
};
|
||||
|
||||
export const DEFAULT_APP_SETTINGS: Settings = {
|
||||
@@ -92,9 +102,10 @@ export async function saveAppSettings(input: {
|
||||
updates: Partial<AppSettings>;
|
||||
deps: SettingsDeps;
|
||||
}): Promise<void> {
|
||||
const current =
|
||||
const storedCurrent =
|
||||
input.queryClient.getQueryData<AppSettings>(APP_SETTINGS_QUERY_KEY) ??
|
||||
(await loadAppSettingsFromStorage(input.deps));
|
||||
const current = normalizeAppSettings(storedCurrent);
|
||||
const next = { ...current, ...input.updates };
|
||||
input.queryClient.setQueryData<AppSettings>(APP_SETTINGS_QUERY_KEY, next);
|
||||
await input.deps.storage.setItem(APP_SETTINGS_KEY, JSON.stringify(next));
|
||||
@@ -104,8 +115,7 @@ export async function loadAppSettingsFromStorage(deps: SettingsDeps): Promise<Ap
|
||||
try {
|
||||
const stored = await deps.storage.getItem(APP_SETTINGS_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored) as Partial<AppSettings>;
|
||||
return { ...DEFAULT_CLIENT_SETTINGS, ...pickAppSettings(parsed) };
|
||||
return normalizeAppSettings(JSON.parse(stored));
|
||||
}
|
||||
|
||||
const legacyStored = await deps.storage.getItem(LEGACY_SETTINGS_KEY);
|
||||
@@ -153,7 +163,29 @@ export async function loadSettingsFromStorage(deps: SettingsDeps): Promise<Setti
|
||||
};
|
||||
}
|
||||
|
||||
function pickAppSettings(stored: Partial<AppSettings>): Partial<AppSettings> {
|
||||
export function normalizeAppSettings(value: unknown): AppSettings {
|
||||
const stored =
|
||||
typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? (value as StoredAppSettings)
|
||||
: {};
|
||||
return { ...DEFAULT_CLIENT_SETTINGS, ...pickAppSettings(stored) };
|
||||
}
|
||||
|
||||
function parseToolCallDetailLevel(stored: StoredAppSettings): ToolCallDetailLevel | null {
|
||||
if (
|
||||
typeof stored.toolCallDetailLevel === "string" &&
|
||||
VALID_TOOL_CALL_DETAIL_LEVELS.has(stored.toolCallDetailLevel)
|
||||
) {
|
||||
return stored.toolCallDetailLevel;
|
||||
}
|
||||
if (typeof stored.compactToolCalls === "boolean") {
|
||||
// COMPAT(compactToolCalls): migrated in v0.1.105, remove after 2027-01-12.
|
||||
return stored.compactToolCalls ? "overview" : "detailed";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function pickAppSettings(stored: StoredAppSettings): Partial<AppSettings> {
|
||||
const result: Partial<AppSettings> = {};
|
||||
if (typeof stored.theme === "string" && VALID_THEMES.has(stored.theme)) {
|
||||
result.theme = stored.theme;
|
||||
@@ -209,6 +241,10 @@ function pickAppSettings(stored: Partial<AppSettings>): Partial<AppSettings> {
|
||||
if (typeof stored.autoExpandReasoning === "boolean") {
|
||||
result.autoExpandReasoning = stored.autoExpandReasoning;
|
||||
}
|
||||
const toolCallDetailLevel = parseToolCallDetailLevel(stored);
|
||||
if (toolCallDetailLevel !== null) {
|
||||
result.toolCallDetailLevel = toolCallDetailLevel;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user