Skip to content

Commit 3e071a1

Browse files
refactor(channel): redesign IM bot model from clients to connections
- Replace WebuiChannelStatus with WebuiChannelConnectionList and WebuiChannelConnection - Add connection lifecycle states: configured, authorizing, bound, error - Split disconnectChannel into disconnectChannelConnection and deleteChannelConnection - Add prepareChannelConnection for creating bots before platform authorization - Introduce connectionId as the identity key replacing name-based lookups - Update runtime bridge surface with listConnections, prepareConnection, deleteConnection - Add unit tests covering list, prepare, disconnect, and delete connection routes
1 parent 8ab2f0c commit 3e071a1

24 files changed

Lines changed: 555 additions & 507 deletions

‎packages/webui/src/client/features/composer/ui/ComposerProjectRow.tsx‎

Lines changed: 28 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { useState } from "react";
1919

2020
import { WebuiDropdown } from "../../../ui/Dropdown.js";
2121
import { WebuiButton } from "../../../ui/button.js";
22-
import { useDialogLayer } from "../../../ui/overlay-listeners.js";
23-
import { WebuiPage } from "../../../ui/page.js";
22+
import { WebuiDialog } from "../../../ui/dialog.js";
23+
import { useWebuiBrowserCapabilities } from "../../../bindings/browser-capabilities.js";
2424
import { workspaceProjectName } from "../../../features/workspace/projection/workspace-name.public.js";
2525
import { WebuiWorkspaceDirectoryBrowser } from "../../../features/workspace/ui/WebuiWorkspaceDirectoryBrowser.public.js";
2626
import { WebuiIconCheck, WebuiIconFolder } from "../../../shell/icons.js";
@@ -45,17 +45,7 @@ export function WebuiComposerProjectRow({
4545
onSelectProject,
4646
}: WebuiComposerProjectRowProps) {
4747
const [browserOpen, setBrowserOpen] = useState(false);
48-
49-
// The directory browser is a dialog, and a dialog that floats above the
50-
// composer is a layer (ADR 0028 § Overlay coordination): the coordinator
51-
// answers the Escape and the press outside it, so the browser holds no
52-
// listener of its own. It is active only while it is on screen.
53-
const browserLayerId = useDialogLayer({
54-
id: "composer-directory-browser",
55-
dismissible: true,
56-
onClose: () => setBrowserOpen(false),
57-
active: browserOpen,
58-
});
48+
const { dom } = useWebuiBrowserCapabilities();
5949

6050
return (
6151
<div className="webui-workspace-bar" data-webui-workspace-toolbar="true">
@@ -112,7 +102,7 @@ export function WebuiComposerProjectRow({
112102
role="option"
113103
aria-selected={active}
114104
data-webui-workspace-recent={dir}
115-
className="webui-workspace-option webui-workspace-option--desktop"
105+
className="webui-workspace-option"
116106
title={dir}
117107
onClick={() => onSelectProject(dir)}
118108
>
@@ -132,7 +122,7 @@ export function WebuiComposerProjectRow({
132122
role="option"
133123
aria-selected={false}
134124
data-webui-workspace-action="add-new"
135-
className="webui-workspace-option webui-workspace-option--desktop"
125+
className="webui-workspace-option"
136126
onClick={() => {
137127
onMenuOpenChange(false);
138128
setBrowserOpen(true);
@@ -148,7 +138,7 @@ export function WebuiComposerProjectRow({
148138
role="option"
149139
aria-selected={projectDir === undefined}
150140
data-webui-workspace-action="no-project"
151-
className="webui-workspace-option webui-workspace-option--desktop"
141+
className="webui-workspace-option"
152142
onClick={() => onSelectProject(undefined)}
153143
>
154144
<WebuiIconFolder className="flex-shrink-0" />
@@ -157,26 +147,29 @@ export function WebuiComposerProjectRow({
157147
</span>
158148
</WebuiButton>
159149
</WebuiDropdown>
160-
<WebuiPage
161-
as="div"
162-
kind="dialog"
163-
present={browserOpen}
164-
motion={{ kind: "surface", state: "enter", direction: "down" }}
165-
role="dialog"
166-
aria-label="选择项目目录"
167-
data-overlay-layer={browserLayerId}
168-
data-webui-workspace-browser-dialog="true"
169-
className="webui-workspace-menu webui-workspace-menu--desktop"
150+
<WebuiDialog
151+
id="composer-directory-browser"
152+
open={browserOpen}
153+
title="选择项目目录"
154+
description="选择要用于新会话的工作目录"
155+
width="standard"
156+
portalTarget={dom.portalTarget()}
157+
onClose={() => setBrowserOpen(false)}
158+
dismissOnEscape
159+
dismissOnBackdrop
160+
testId="webui-workspace-browser-dialog"
161+
dataAttributes={{ "data-webui-workspace-browser-dialog": "true" }}
162+
bodyClassName="webui-workspace-browser-dialog-body"
170163
>
171-
<WebuiWorkspaceDirectoryBrowser
172-
initialDir={projectDir}
173-
onSelect={(dir) => {
174-
setBrowserOpen(false);
175-
onSelectProject(dir);
176-
}}
177-
onCancel={() => setBrowserOpen(false)}
178-
/>
179-
</WebuiPage>
164+
<WebuiWorkspaceDirectoryBrowser
165+
initialDir={projectDir}
166+
onSelect={(dir) => {
167+
setBrowserOpen(false);
168+
onSelectProject(dir);
169+
}}
170+
onCancel={() => setBrowserOpen(false)}
171+
/>
172+
</WebuiDialog>
180173
</div>
181174
</div>
182175
);

‎packages/webui/src/client/features/composer/ui/composer.css‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -550,15 +550,8 @@
550550
color: var(--text_default_tertiary);
551551
opacity: 0.55;
552552
}
553-
/* The directory browser keeps the shared `.webui-workspace-menu` panel, which
554-
* is still anchored to this row. The recent list is a dropdown now: the
555-
* shared layer gives it the surface and the placement, and this says only
556-
* what the list itself needs. */
557-
.webui-workspace-menu {
558-
right: auto;
559-
left: 0;
560-
min-width: 220px;
561-
}
553+
/* The recent list is a shared dropdown surface; this says only what its
554+
* workspace rows need. The directory browser uses the shared dialog shell. */
562555
.webui-workspace-picker-menu {
563556
display: flex;
564557
min-width: 220px;

‎packages/webui/src/client/features/scheduling/ui/scheduling-editor.tsx‎

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
// shared row cannot disagree about what the preset means.
1111
import { useState, type ReactElement } from "react";
1212
import { WebuiInline } from "../../../ui/layout-primitives.js";
13+
import { useWebuiBrowserCapabilities } from "../../../bindings/browser-capabilities.js";
14+
import { WebuiDialog } from "../../../ui/dialog.js";
1315

1416
import type {
1517
WebuiCronSchedule,
@@ -28,7 +30,7 @@ import {
2830
type WebuiCronSchedulePreset,
2931
} from "../projection/scheduling-panel.js";
3032
import { WebuiButton } from "../../../ui/button.js";
31-
import { WebuiField, WebuiFormLayout, WebuiSelect, WebuiTextarea, WebuiTextInput } from "../../../ui/field.js";
33+
import { WebuiField, WebuiFormLayout, WebuiSelectMenu, WebuiTextarea, WebuiTextInput } from "../../../ui/field.js";
3234

3335
export interface SchedulingEditorViewProps {
3436
readonly mode: "create" | "edit";
@@ -51,7 +53,8 @@ export function SchedulingEditorView(props: SchedulingEditorViewProps): ReactEle
5153
event.preventDefault();
5254
props.onSubmit();
5355
}}
54-
className="mb-4 rounded-[8px] border border-border_default p-3"
56+
id="webui-scheduling-editor-form"
57+
className="flex flex-col gap-4"
5558
>
5659
<WebuiFormLayout>
5760
<WebuiField
@@ -100,20 +103,19 @@ export function SchedulingEditorView(props: SchedulingEditorViewProps): ReactEle
100103
/>
101104
<WebuiField
102105
label="周期"
103-
control={(props) => (
104-
<WebuiSelect
105-
{...props}
106-
controlSize="dense"
106+
control={(controlProps, field) => (
107+
<WebuiSelectMenu
108+
{...controlProps}
109+
labelId={field.labelId}
110+
className="webui-control--dense"
107111
data-webui-scheduling-field="preset"
112+
options={(Object.keys(WEBUI_CRON_PRESET_LABELS) as WebuiCronSchedulePreset[]).map((preset) => ({
113+
value: preset,
114+
label: WEBUI_CRON_PRESET_LABELS[preset],
115+
}))}
108116
value={draft.preset}
109-
onChange={(event) => onChange({ preset: event.target.value as WebuiCronSchedulePreset })}
110-
>
111-
{(Object.keys(WEBUI_CRON_PRESET_LABELS) as WebuiCronSchedulePreset[]).map((preset) => (
112-
<option key={preset} value={preset}>
113-
{WEBUI_CRON_PRESET_LABELS[preset]}
114-
</option>
115-
))}
116-
</WebuiSelect>
117+
onChange={(value) => onChange({ preset: value as WebuiCronSchedulePreset })}
118+
/>
117119
)}
118120
/>
119121
{draft.preset === "daily" || draft.preset === "weekly"
@@ -242,21 +244,6 @@ export function SchedulingEditorView(props: SchedulingEditorViewProps): ReactEle
242244
{props.error}
243245
</p>
244246
: null}
245-
<WebuiInline gap="small" className="pt-1">
246-
<WebuiButton variant="outline" size="inline" type="submit"
247-
data-webui-scheduling-submit="true"
248-
disabled={props.busy}
249-
250-
>
251-
{props.mode === "create" ? "创建" : "保存"}
252-
</WebuiButton>
253-
<WebuiButton variant="quiet-secondary" size="inline" data-webui-scheduling-cancel="true"
254-
onClick={props.onCancel}
255-
256-
>
257-
取消
258-
</WebuiButton>
259-
</WebuiInline>
260247
</WebuiFormLayout>
261248
</form>
262249
);
@@ -265,18 +252,22 @@ export function SchedulingEditorView(props: SchedulingEditorViewProps): ReactEle
265252
export function SchedulingEditor(props: {
266253
readonly owner: WebuiSchedulingOwner;
267254
readonly task?: WebuiCronTask;
255+
readonly open: boolean;
256+
readonly seed?: Partial<WebuiCronDraft>;
268257
/** The agent a new task belongs to; the panel passes the client's own. */
269258
readonly agentName?: string;
270259
readonly nowMs: () => number;
271260
readonly onDone: () => void;
261+
readonly onExitComplete: () => void;
272262
}): ReactElement {
263+
const { dom } = useWebuiBrowserCapabilities();
273264
// Read once: a narrowed `props.task` inside the submit closure loses the narrowing.
274265
const editingTask = props.task;
275266
const mode = editingTask ? "edit" : "create";
276267
const [initialDraft] = useState<WebuiCronDraft>(() =>
277268
editingTask
278269
? webuiCronDraftFromTask(editingTask)
279-
: { ...WEBUI_CRON_EMPTY_DRAFT, agentName: props.agentName ?? "" },
270+
: { ...WEBUI_CRON_EMPTY_DRAFT, agentName: props.agentName ?? "", ...(props.seed ?? {}) },
280271
);
281272
const [draft, setDraft] = useState<WebuiCronDraft>(initialDraft);
282273
const [error, setError] = useState<string | undefined>(undefined);
@@ -324,14 +315,41 @@ export function SchedulingEditor(props: {
324315
};
325316

326317
return (
327-
<SchedulingEditorView
328-
mode={mode}
329-
draft={draft}
330-
busy={busy}
331-
{...(error === undefined ? {} : { error })}
332-
onChange={(patch) => setDraft((current) => ({ ...current, ...patch }))}
333-
onCancel={props.onDone}
334-
onSubmit={() => void submit()}
335-
/>
318+
<WebuiDialog
319+
id="webui-scheduling-editor-dialog"
320+
open={props.open}
321+
title={editingTask ? "编辑定时任务" : "新建定时任务"}
322+
description={editingTask ? "调整任务内容和执行计划" : "设置任务指令与自动执行计划"}
323+
width="wide"
324+
portalTarget={dom.portalTarget()}
325+
onClose={props.onDone}
326+
onExitComplete={props.onExitComplete}
327+
dismissOnEscape
328+
dismissOnBackdrop
329+
testId="webui-scheduling-editor-dialog"
330+
footer={(
331+
<WebuiInline gap="small">
332+
<WebuiButton variant="secondary" size="dialog" type="button" data-webui-scheduling-cancel="true" onClick={props.onDone}>
333+
取消
334+
</WebuiButton>
335+
<WebuiButton variant="primary" size="dialog" type="submit" form="webui-scheduling-editor-form"
336+
data-webui-scheduling-submit="true" loading={busy}
337+
loadingLabel={editingTask ? "保存中…" : "创建中…"}
338+
>
339+
{editingTask ? "保存" : "创建任务"}
340+
</WebuiButton>
341+
</WebuiInline>
342+
)}
343+
>
344+
<SchedulingEditorView
345+
mode={mode}
346+
draft={draft}
347+
busy={busy}
348+
{...(error === undefined ? {} : { error })}
349+
onChange={(patch) => setDraft((current) => ({ ...current, ...patch }))}
350+
onCancel={props.onDone}
351+
onSubmit={() => void submit()}
352+
/>
353+
</WebuiDialog>
336354
);
337355
}

0 commit comments

Comments
 (0)