Skip to content

Commit 231157d

Browse files
feat(session): add app mode preference to session creation
- Add appMode field to session contracts (coding | work) - Add server-side validation for appMode in session operation - Wire appModePreference through create-application and main.tsx - Carry appMode into task-session-workflows at session creation - Add mode selection UI in settings with accessible radiogroup - Refactor AgentAvatar size mapping into a constant record - Auto-select first agent in management and reduce detail avatar to small - Enable independent scrolling for agent list and detail panels - Reduce usage popover skeleton rows from 8 to 4 for size stability
1 parent 8d839e8 commit 231157d

18 files changed

Lines changed: 365 additions & 62 deletions

File tree

‎.plan/browser-use/complete/isolated-chromium.acceptance.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
},
1717
"observed": "a pointer click and a text input both arrived in the page the provider was driving",
1818
"result": "pass",
19-
"at": "2026-11-04T01:33:54.942Z",
19+
"at": "2026-11-04T05:12:34.417Z",
2020
"os": "linux 7.0.0-38-generic",
2121
"suite": "isolated-chromium"
2222
},
@@ -37,7 +37,7 @@
3737
},
3838
"observed": "the stale identity was refused with browser_target_stale and the page that replaced it received nothing",
3939
"result": "pass",
40-
"at": "2026-11-04T01:33:56.455Z",
40+
"at": "2026-11-04T05:12:35.938Z",
4141
"os": "linux 7.0.0-38-generic",
4242
"suite": "isolated-chromium"
4343
}

‎.plan/workspace-browser-tab/complete/workspace-browser-tab.acceptance.json‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"observed": "the run's selection points at the page the Agent opened, the runtime publishes it, and the strip's reconciliation makes it the active panel tab",
1616
"result": "pass",
17-
"at": "2026-11-04T01:31:52.520Z",
17+
"at": "2026-11-04T05:11:13.237Z",
1818
"os": "linux 7.0.0-38-generic",
1919
"suite": "workspace-browser-tab"
2020
},
@@ -33,7 +33,7 @@
3333
},
3434
"observed": "the page is at the requested url and the frame stream follows the page it opened",
3535
"result": "pass",
36-
"at": "2026-11-04T01:31:53.699Z",
36+
"at": "2026-11-04T05:11:14.418Z",
3737
"os": "linux 7.0.0-38-generic",
3838
"suite": "workspace-browser-tab"
3939
},
@@ -52,7 +52,7 @@
5252
},
5353
"observed": "the action runs on the run's selection, and a reference recorded before the page moved is refused",
5454
"result": "pass",
55-
"at": "2026-11-04T01:31:54.568Z",
55+
"at": "2026-11-04T05:11:15.288Z",
5656
"os": "linux 7.0.0-38-generic",
5757
"suite": "workspace-browser-tab"
5858
},
@@ -71,7 +71,7 @@
7171
},
7272
"observed": "the Agent's page and the person's page are different, and each shows its own picture",
7373
"result": "pass",
74-
"at": "2026-11-04T01:31:54.637Z",
74+
"at": "2026-11-04T05:11:15.350Z",
7575
"os": "linux 7.0.0-38-generic",
7676
"suite": "workspace-browser-tab"
7777
},
@@ -90,7 +90,7 @@
9090
},
9191
"observed": "the run's selection becomes the person's page, so the Agent can act on it",
9292
"result": "pass",
93-
"at": "2026-11-04T01:31:55.583Z",
93+
"at": "2026-11-04T05:11:16.257Z",
9494
"os": "linux 7.0.0-38-generic",
9595
"suite": "workspace-browser-tab"
9696
},
@@ -109,7 +109,7 @@
109109
},
110110
"observed": "the input reaches the person's page, not the Agent's",
111111
"result": "pass",
112-
"at": "2026-11-04T01:31:56.403Z",
112+
"at": "2026-11-04T05:11:17.095Z",
113113
"os": "linux 7.0.0-38-generic",
114114
"suite": "workspace-browser-tab"
115115
},
@@ -128,7 +128,7 @@
128128
},
129129
"observed": "the tab becomes that page, and the url is shown read-only in the toolbar",
130130
"result": "pass",
131-
"at": "2026-11-04T01:31:56.429Z",
131+
"at": "2026-11-04T05:11:17.130Z",
132132
"os": "linux 7.0.0-38-generic",
133133
"suite": "workspace-browser-tab"
134134
},
@@ -147,7 +147,7 @@
147147
},
148148
"observed": "the page is released, and the runtime's page set follows the strip",
149149
"result": "pass",
150-
"at": "2026-11-04T01:31:57.213Z",
150+
"at": "2026-11-04T05:11:17.919Z",
151151
"os": "linux 7.0.0-38-generic",
152152
"suite": "workspace-browser-tab"
153153
},
@@ -166,7 +166,7 @@
166166
},
167167
"observed": "the page moves through its own history, and the generation follows",
168168
"result": "pass",
169-
"at": "2026-11-04T01:31:59.855Z",
169+
"at": "2026-11-04T05:11:20.581Z",
170170
"os": "linux 7.0.0-38-generic",
171171
"suite": "workspace-browser-tab"
172172
},
@@ -185,7 +185,7 @@
185185
},
186186
"observed": "the Agent's open_tab and the person's launcher path are both refused with browser_surface_not_authorized",
187187
"result": "pass",
188-
"at": "2026-11-04T01:31:59.857Z",
188+
"at": "2026-11-04T05:11:20.582Z",
189189
"os": "linux 7.0.0-38-generic",
190190
"suite": "workspace-browser-tab"
191191
},
@@ -204,7 +204,7 @@
204204
},
205205
"observed": "every action except open_tab is refused with browser_target_unknown",
206206
"result": "pass",
207-
"at": "2026-11-04T01:31:59.877Z",
207+
"at": "2026-11-04T05:11:20.603Z",
208208
"os": "linux 7.0.0-38-generic",
209209
"suite": "workspace-browser-tab"
210210
},
@@ -223,7 +223,7 @@
223223
},
224224
"observed": "its pages are released and no page is left behind",
225225
"result": "pass",
226-
"at": "2026-11-04T01:32:00.657Z",
226+
"at": "2026-11-04T05:11:21.384Z",
227227
"os": "linux 7.0.0-38-generic",
228228
"suite": "workspace-browser-tab"
229229
},
@@ -242,7 +242,7 @@
242242
},
243243
"observed": "both are addressable, and the strip shows both",
244244
"result": "pass",
245-
"at": "2026-11-04T01:32:01.585Z",
245+
"at": "2026-11-04T05:11:22.304Z",
246246
"os": "linux 7.0.0-38-generic",
247247
"suite": "workspace-browser-tab"
248248
},
@@ -261,7 +261,7 @@
261261
},
262262
"observed": "its generation advances, and a reference recorded against the old generation is refused — on every tab, not only the first",
263263
"result": "pass",
264-
"at": "2026-11-04T01:32:02.505Z",
264+
"at": "2026-11-04T05:11:23.227Z",
265265
"os": "linux 7.0.0-38-generic",
266266
"suite": "workspace-browser-tab"
267267
}

‎packages/webui/src/client/application/create-application.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,15 @@ export interface WebuiApplicationDeps {
190190
readonly readNoProjectFlag?: () => boolean;
191191
readonly writeNoProjectFlag?: (value: boolean) => void;
192192
};
193+
/**
194+
* The application mode (设置 › 通用 › 模式) this client remembers.
195+
*
196+
* Read when a session is created, so the choice takes effect on the next
197+
* session rather than on a running one — the runtime reads it at creation.
198+
*/
199+
readonly appModePreference?: {
200+
readonly read: () => "coding" | "work";
201+
};
193202
/** Seed for the composer store (SSR / tests). */
194203
readonly composer?: WebuiComposerStore;
195204
/**
@@ -920,6 +929,9 @@ export function createWebuiApplication(
920929
// goal path read it before this became the one owner. `WebuiTransport` satisfies both slices, so
921930
// production has one source; a host that wires only one of them still creates here.
922931
...(slices.sessions.createSession ? { createSession: slices.sessions.createSession } : {}),
932+
// The remembered mode reaches session creation through the host: the
933+
// preference is browser storage, which this layer may not read itself.
934+
...(deps.appModePreference ? { readAppMode: deps.appModePreference.read } : {}),
923935
storage: {
924936
readNoProjectFlag: () => deps.workspacePreference?.readNoProjectFlag?.() ?? false,
925937
writeNoProjectFlag: (value) => deps.workspacePreference?.writeNoProjectFlag?.(value),

‎packages/webui/src/client/application/task-session-workflows.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ export interface WebuiTaskSessionDeps {
6161
readonly catalogue: WebuiTaskSessionCatalogue;
6262
/** The creation capability. Absent means this host cannot create at all. */
6363
readonly createSession?: WebuiClientSessionCreator;
64+
/**
65+
* The application mode this client remembers, read at creation time.
66+
*
67+
* Injected rather than imported: the remembered choice lives in browser
68+
* storage, which is the composition root's business, and this workflow stays
69+
* free of the platform layer (the layer rule forbids the edge).
70+
*/
71+
readonly readAppMode?: () => "coding" | "work";
6472
/**
6573
* Record the Agent Team choice the created session was started with. Sessions
6674
* owns that choice, so this asks for the record rather than writing it here.
@@ -234,6 +242,10 @@ export function createWebuiTaskSessionWorkflows(
234242
name: input.name ?? "main",
235243
...(input.workspaceDir !== undefined ? { workspaceDir: input.workspaceDir } : {}),
236244
teamModeOff: input.teamModeOff,
245+
// What the session runs as. The runtime resolves the mode to the
246+
// session's prompts and to the capabilities it gates, so a session
247+
// created in 通用 › 日常工作 behaves like the Desktop's.
248+
...(deps.readAppMode ? { appMode: deps.readAppMode() } : {}),
237249
});
238250
} catch (error) {
239251
pendingHomeIncarnation = undefined;

‎packages/webui/src/client/features/account/ui/UserMenuUsage.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function UsageSkeleton({ loading }: { readonly loading?: boolean }): ReactElemen
3131
className="webui-user-menu-usage-skeleton"
3232
data-testid={loading ? "usage-popover-loading" : "usage-popover-pending"}
3333
>
34-
<WebuiSkeletonGroup rows={8} />
34+
<WebuiSkeletonGroup rows={4} />
3535
</WebuiStack>
3636
);
3737
}

‎packages/webui/src/client/features/plugins/ui/AgentForm.tsx‎

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useState, type ReactElement } from "react";
88

99
import {
1010
readWebuiPluginRow as read,
11+
readWebuiAgentModel,
1112
type WebuiPluginRow as Row,
1213
} from "../contracts/plugin-rows.js";
1314
import type { WebuiPluginIntent } from "../contracts/plugin-actions.js";
@@ -35,7 +36,7 @@ export function AgentForm({
3536
);
3637
const [description, setDescription] = useState(read(editing ?? {}, "description"));
3738
const [prompt, setPrompt] = useState(read(editing ?? {}, "systemPrompt", "system_prompt"));
38-
const [model, setModel] = useState(read(editing ?? {}, "model"));
39+
const [model, setModel] = useState(readWebuiAgentModel(editing ?? {}));
3940
const { errors, validateRequired, clearFieldError } = useWebuiFormValidation();
4041

4142
const save = async () => {
@@ -89,32 +90,26 @@ export function AgentForm({
8990
<WebuiTextInput {...props} value={description} placeholder="例如:帮助检查代码质量和潜在问题" onChange={(event) => setDescription(event.currentTarget.value)} />
9091
)}
9192
/>
93+
<WebuiField
94+
label="模型"
95+
control={(props) => (
96+
<WebuiModelSelect
97+
{...props}
98+
value={model}
99+
onChange={setModel}
100+
formatValue={webuiBareModelValue}
101+
placeholder="使用默认模型"
102+
defaultLabel="使用默认模型"
103+
/>
104+
)}
105+
/>
92106
<WebuiField
93107
className="wide"
94108
label="系统提示词"
95109
control={(props) => (
96110
<WebuiTextarea {...props} value={prompt} placeholder="描述 Agent 应如何工作和回应" onChange={(event) => setPrompt(event.currentTarget.value)} />
97111
)}
98112
/>
99-
{/* The model belongs to the initial definition, which only a new agent
100-
sends; an edit updates the agent's fields and leaves it alone. The
101-
field is the catalogue's own list: a definition stores a model name,
102-
so it writes the bare id and offers the empty value back. */}
103-
{editing ? null : (
104-
<WebuiField
105-
label="模型"
106-
control={(props) => (
107-
<WebuiModelSelect
108-
{...props}
109-
value={model}
110-
onChange={setModel}
111-
formatValue={webuiBareModelValue}
112-
placeholder="使用默认模型"
113-
defaultLabel="使用默认模型"
114-
/>
115-
)}
116-
/>
117-
)}
118113
<WebuiDialogActions>
119114
{editing && onChatWithAgent ? (
120115
<WebuiButton variant="secondary" size="dialog" onClick={() => onChatWithAgent(name)}>

‎packages/webui/src/client/features/plugins/ui/PluginCatalogue.tsx‎

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { WebuiDropdown } from "../../../ui/Dropdown.js";
1010
import { WebuiMenuItem } from "../../../ui/menu-item.js";
1111
import {
1212
readWebuiPluginRow as read,
13+
readWebuiAgentModel,
1314
type WebuiPluginRow as Row,
1415
} from "../contracts/plugin-rows.js";
1516
import { PluginImportDialog } from "./PluginImportDialog.js";
@@ -285,6 +286,9 @@ export function PluginCatalogue({
285286
"displayName",
286287
"display_name",
287288
);
289+
const activeAgent = selectedAgent
290+
? filtered.find((item) => nameOf(item) === nameOf(selectedAgent)) ?? filtered[0]
291+
: filtered[0];
288292
const chatWithAgent = async (agent: Row) => {
289293
if (!onChatWithAgent) return;
290294
try {
@@ -555,7 +559,7 @@ export function PluginCatalogue({
555559
size="row"
556560
className="webui-agent-list-item w-full gap-2.5 overflow-hidden"
557561
key={nameOf(item)}
558-
aria-pressed={selectedAgent === item}
562+
aria-pressed={activeAgent !== undefined && nameOf(activeAgent) === nameOf(item)}
559563
onClick={() => {
560564
attachItemDialog();
561565
setEditing(item);
@@ -593,13 +597,13 @@ export function PluginCatalogue({
593597
<div className="webui-agent-fields">
594598
<div className="webui-agent-avatar-field">
595599
头像
596-
<WebuiAgentAvatar agent={selectedAgent} size="large" />
600+
<WebuiAgentAvatar agent={activeAgent} size="small" />
597601
</div>
598602
<div>
599603
名称
600604
<span data-testid="webui-agent-detail-name">
601605
{read(
602-
selectedAgent ?? {},
606+
activeAgent ?? {},
603607
"name",
604608
"displayName",
605609
"display_name",
@@ -609,30 +613,30 @@ export function PluginCatalogue({
609613
<div className="webui-agent-model-field">
610614
模型
611615
<span data-testid="webui-agent-detail-model">
612-
{read(selectedAgent ?? {}, "model") || "—"}
616+
{readWebuiAgentModel(activeAgent ?? {}) || "—"}
613617
</span>
614618
</div>
615619
<div>
616620
描述
617621
<span data-testid="webui-agent-detail-description">
618-
{read(selectedAgent ?? {}, "description") || "—"}
622+
{read(activeAgent ?? {}, "description") || "—"}
619623
</span>
620624
</div>
621625
<div>
622626
系统提示词
623627
<span data-testid="webui-agent-detail-prompt">
624-
{read(selectedAgent ?? {}, "systemPrompt", "system_prompt")}
628+
{read(activeAgent ?? {}, "systemPrompt", "system_prompt")}
625629
</span>
626630
</div>
627631
<div className="webui-agent-actions">
628-
{selectedAgent ? (
632+
{activeAgent ? (
629633
<WebuiButton
630634
variant="danger"
631635
size="compact"
632636
onClick={() =>
633637
confirmMutation("删除 Agent", {
634638
kind: "deleteAgent",
635-
name: nameOf(selectedAgent),
639+
name: nameOf(activeAgent),
636640
})
637641
}
638642
>
@@ -644,21 +648,21 @@ export function PluginCatalogue({
644648
<WebuiButton
645649
variant="outline"
646650
size="compact"
647-
disabled={busy || !selectedAgent}
651+
disabled={busy || !activeAgent}
648652
onClick={() => {
649653
attachItemDialog();
650-
setEditing(selectedAgent);
654+
setEditing(activeAgent);
651655
setDialog("agent");
652656
}}
653657
>
654658
编辑
655659
</WebuiButton>
656-
{selectedAgent && onChatWithAgent ? (
660+
{activeAgent && onChatWithAgent ? (
657661
<WebuiButton
658662
variant="surface-quiet"
659663
size="compact"
660664
disabled={busy}
661-
onClick={() => chatWithAgent(selectedAgent)}
665+
onClick={() => chatWithAgent(activeAgent)}
662666
>
663667
和他对话
664668
</WebuiButton>

0 commit comments

Comments
 (0)