Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions apps/desktop/src/components/settings/general-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
type RenderingFidelity,
type Theme,
} from "@llm-space/ui/components/theme-provider";
import {
setCollapseProcessGroups,
useCollapseProcessGroups,
} from "@llm-space/ui/components/thread-playground/message/process-groups";
import { ModelAvatar } from "@llm-space/ui/components/thread-playground/model-avatar";
import { Button } from "@llm-space/ui/ui/button";
import {
Expand Down Expand Up @@ -47,6 +51,7 @@ import { DEFAULT_UPDATE_MODE, type UpdateMode } from "@/shared/updates";

import { PrimaryColorPicker } from "./primary-color-picker";
import { SettingsPage } from "./settings-page";
import { SettingsToggleRow } from "./settings-toggle-row";

/** Sentinel value for the "Automatic (first available model)" option. */
const AUTO_DEFAULT_MODEL = "__auto__";
Expand Down Expand Up @@ -298,6 +303,23 @@ function useUpdateMode(): [UpdateMode, (mode: UpdateMode) => void] {
return [mode, change];
}

/**
* Opt into collapsing each finished run's intermediate steps (thinking, tool
* calls) into one expandable row once the result arrives. On by default.
*/
function CollapseProcessGroupsRow() {
const { t } = useI18n();
const collapse = useCollapseProcessGroups();
return (
<SettingsToggleRow
title={t.general.collapseProcessGroups}
hint={t.general.collapseProcessGroupsHint}
checked={collapse}
onCheckedChange={setCollapseProcessGroups}
/>
);
}

export function GeneralPage() {
const { lang, setLang, t } = useI18n();
const { theme, setTheme } = useTheme();
Expand Down Expand Up @@ -427,6 +449,10 @@ export function GeneralPage() {
</SettingsRow>
</SettingsSection>

<SettingsSection title={t.general.conversations}>
<CollapseProcessGroupsRow />
</SettingsSection>

<SettingsSection title={t.general.dataPrivacy}>
<SettingsRow
label={
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ const APP_MESSAGES = {
"Used for new threads, and when a thread's model is no longer available.",
defaultModelAutomatic: "Automatic",
defaultModelAria: "Default model",
conversations: "Messages",
collapseProcessGroups: "Collapse process steps",
collapseProcessGroupsHint:
"After a run finishes, collapse its intermediate steps (thinking and tool calls) into one expandable row so the result comes first.",
dataPrivacy: "Data & privacy",
workspaceFolder: "Workspace folder",
workspaceFolderHint: "Where your threads are stored on disk.",
Expand Down Expand Up @@ -800,6 +804,10 @@ const APP_MESSAGES = {
defaultModelHint: "用于新建 Thread,以及 Thread 原有模型不可用时。",
defaultModelAutomatic: "自动",
defaultModelAria: "默认模型",
conversations: "消息",
collapseProcessGroups: "自动折叠过程步骤",
collapseProcessGroupsHint:
"运行结束后,将中间过程(思考和工具调用)折叠为可展开的一栏,优先展示结果。",
dataPrivacy: "数据与隐私",
workspaceFolder: "工作区文件夹",
workspaceFolderHint: "Thread 在磁盘上的存储位置。",
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/src/i18n/playground-labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,10 @@ export const PLAYGROUND_LABELS: Record<AppLanguage, PlaygroundLabels> = {
previewText: "Preview text content",
noText: "No text content",
},
processGroups: {
lastTool: (name) => `Last: ${name}`,
errors: (count) => `${count} failed`,
},
messages: {
user: "User",
assistant: "Assistant",
Expand Down Expand Up @@ -664,6 +668,10 @@ export const PLAYGROUND_LABELS: Record<AppLanguage, PlaygroundLabels> = {
previewText: "预览文本内容",
noText: "没有文本内容",
},
processGroups: {
lastTool: (name) => `最后:${name}`,
errors: (count) => `${count} 步失败`,
},
messages: {
user: "用户",
assistant: "助手",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"./components/thread-playground": "./src/components/thread-playground/index.tsx",
"./components/thread-playground/playground-labels": "./src/components/thread-playground/playground-labels.tsx",
"./components/thread-playground/examples/prompts": "./src/components/thread-playground/examples/prompts.ts",
"./components/thread-playground/message/process-groups": "./src/components/thread-playground/message/process-groups.ts",
"./components/code-editor": "./src/components/code-editor/index.tsx",
"./components/*": "./src/components/*.tsx",
"./styles/globals.css": "./src/styles/globals.css",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { AssistantMessage, Message } from "@llm-space/core";

import type { DisplayRow, ProcessGroupSpan } from "./process-groups";
import { findProcessGroupSpans } from "./process-groups";

export interface DisplayMessage {
message: Message;
streaming: boolean;
Expand Down Expand Up @@ -43,3 +46,57 @@ export function resolveDisplayMessages(
});
return rows;
}

/**
* Fold the display rows into a render sequence where completed cross-message
* process groups collapse into a single header row (or render header-first
* when the user expanded the group). Grouping only wraps runs that ended with
* a result; trailing member runs — including the live streaming message —
* always render as plain rows.
*/
export function resolveDisplayRows(
messages: readonly Message[],
streamingMessageId: string | null,
running: boolean,
options: {
/** Master switch (setting + only while the thread is idle). */
groupingEnabled?: boolean;
/** Groups the user manually expanded; ids are first-member message ids. */
expandedGroupIds?: readonly string[];
} = {}
): DisplayRow[] {
const base = resolveDisplayMessages(messages, streamingMessageId, running);
if (!options.groupingEnabled) {
return base.map((row) => ({ kind: "message" as const, ...row }));
}
const expanded = new Set(options.expandedGroupIds ?? []);
const spansByStart = new Map<number, ProcessGroupSpan>(
findProcessGroupSpans(base.map((row) => row.message)).map((span) => [
span.start,
span,
])
);
const rows: DisplayRow[] = [];
let index = 0;
while (index < base.length) {
const span = spansByStart.get(index);
// Never wrap a group that contains a live streaming row.
if (
span &&
!base.slice(span.start, span.end).some((row) => row.streaming)
) {
const collapsed = !expanded.has(span.group.id);
rows.push({ kind: "processGroup", group: span.group, collapsed });
if (!collapsed) {
for (const row of base.slice(span.start, span.end)) {
rows.push({ kind: "message", ...row });
}
}
index = span.end;
continue;
}
rows.push({ kind: "message", ...base[index] });
index += 1;
}
return rows;
}
Loading