@dsh-workbench/shared 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/dsh.d.ts +1 -0
- package/dist/api/host-write.d.ts +1 -1
- package/dist/components/FormRows.css +10 -4
- package/dist/components/Rows.css +12 -5
- package/dist/components/SelectField.css +20 -0
- package/dist/components/SelectField.d.ts +3 -1
- package/dist/components/SelectField.js +43 -25
- package/dist/components/SelectFieldNodes.d.ts +21 -0
- package/dist/components/SelectFieldNodes.js +17 -0
- package/dist/config/constants.d.ts +3 -0
- package/dist/config/constants.js +3 -0
- package/dist/host/http.d.ts +3 -0
- package/dist/host/http.js +10 -1
- package/dist/host/tool-kit.d.ts +1 -0
- package/dist/host/tool-kit.js +4 -0
- package/dist/host.d.ts +2 -0
- package/dist/host.js +2 -0
- package/dist/i18n/en.js +14 -0
- package/dist/i18n/index.d.ts +15 -1
- package/dist/i18n/zh.d.ts +14 -0
- package/dist/i18n/zh.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/board-fact.d.ts +13 -0
- package/dist/lib/board-fact.js +28 -0
- package/dist/lib/diy-debug.d.ts +39 -0
- package/dist/lib/diy-debug.js +70 -0
- package/package.json +1 -1
- package/src/api/dsh.ts +6 -1
- package/src/api/host-write.ts +5 -1
- package/src/components/FormRows.css +10 -4
- package/src/components/Rows.css +12 -5
- package/src/components/SelectField.css +20 -0
- package/src/components/SelectField.test.tsx +63 -0
- package/src/components/SelectField.tsx +56 -81
- package/src/components/SelectFieldNodes.tsx +77 -0
- package/src/config/constants.ts +5 -0
- package/src/host/http.ts +16 -3
- package/src/host/tool-kit.ts +5 -0
- package/src/host.ts +2 -0
- package/src/i18n/en.ts +15 -0
- package/src/i18n/zh.ts +15 -0
- package/src/index.ts +2 -0
- package/src/lib/board-fact.test.ts +86 -0
- package/src/lib/board-fact.ts +45 -0
- package/src/lib/diy-debug.test.ts +110 -0
- package/src/lib/diy-debug.ts +118 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type DebugPhase = 'debugging' | 'paused';
|
|
2
|
+
export type DebugIntent = 'pause' | 'resume' | 'stop';
|
|
3
|
+
export declare function parseDebugIntent(value: unknown): DebugIntent | null;
|
|
4
|
+
export declare const DEBUG_STALE_MS: number;
|
|
5
|
+
export interface DebugState {
|
|
6
|
+
readonly phase: DebugPhase;
|
|
7
|
+
readonly sessionId: string;
|
|
8
|
+
readonly workspace: string;
|
|
9
|
+
readonly board: string;
|
|
10
|
+
readonly unit: string;
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly startedAt: number;
|
|
13
|
+
readonly at: number;
|
|
14
|
+
}
|
|
15
|
+
export interface DebugPact {
|
|
16
|
+
readonly sessionId: string;
|
|
17
|
+
readonly workspace: string;
|
|
18
|
+
readonly board: string;
|
|
19
|
+
readonly unit: string;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly now: number;
|
|
22
|
+
}
|
|
23
|
+
export type DebugRefusal = 'already-held' | 'held-by-other' | 'not-owner' | 'none' | 'already-paused' | 'already-debugging';
|
|
24
|
+
export type DebugMove = {
|
|
25
|
+
readonly ok: true;
|
|
26
|
+
readonly state: DebugState;
|
|
27
|
+
} | {
|
|
28
|
+
readonly ok: false;
|
|
29
|
+
readonly code: DebugRefusal;
|
|
30
|
+
};
|
|
31
|
+
export interface DebugStore {
|
|
32
|
+
live(now: number): DebugState | null;
|
|
33
|
+
begin(pact: DebugPact): DebugMove;
|
|
34
|
+
pause(sessionId: string, now: number): DebugMove;
|
|
35
|
+
resume(sessionId: string, now: number): DebugMove;
|
|
36
|
+
stop(sessionId: string, now: number): DebugMove;
|
|
37
|
+
}
|
|
38
|
+
export declare function createDebugStore(): DebugStore;
|
|
39
|
+
export declare function debugOwnerText(state: DebugState): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export function parseDebugIntent(value) {
|
|
2
|
+
return value === 'pause' || value === 'resume' || value === 'stop' ? value : null;
|
|
3
|
+
}
|
|
4
|
+
export const DEBUG_STALE_MS = 4 * 60 * 60 * 1000;
|
|
5
|
+
function alive(state, now) {
|
|
6
|
+
if (state === null)
|
|
7
|
+
return null;
|
|
8
|
+
return now - state.at <= DEBUG_STALE_MS ? state : null;
|
|
9
|
+
}
|
|
10
|
+
function ownerOf(state, sessionId, now) {
|
|
11
|
+
const held = alive(state, now);
|
|
12
|
+
if (held === null)
|
|
13
|
+
return 'none';
|
|
14
|
+
return held.sessionId === sessionId ? held : 'not-owner';
|
|
15
|
+
}
|
|
16
|
+
export function createDebugStore() {
|
|
17
|
+
let held = null;
|
|
18
|
+
const live = (now) => alive(held, now);
|
|
19
|
+
return {
|
|
20
|
+
live,
|
|
21
|
+
begin(pact) {
|
|
22
|
+
const current = alive(held, pact.now);
|
|
23
|
+
if (current !== null) {
|
|
24
|
+
return { ok: false, code: current.sessionId === pact.sessionId ? 'already-held' : 'held-by-other' };
|
|
25
|
+
}
|
|
26
|
+
const state = {
|
|
27
|
+
phase: 'debugging',
|
|
28
|
+
sessionId: pact.sessionId,
|
|
29
|
+
workspace: pact.workspace,
|
|
30
|
+
board: pact.board,
|
|
31
|
+
unit: pact.unit,
|
|
32
|
+
name: pact.name,
|
|
33
|
+
startedAt: pact.now,
|
|
34
|
+
at: pact.now,
|
|
35
|
+
};
|
|
36
|
+
held = state;
|
|
37
|
+
return { ok: true, state };
|
|
38
|
+
},
|
|
39
|
+
pause(sessionId, now) {
|
|
40
|
+
const found = ownerOf(held, sessionId, now);
|
|
41
|
+
if (typeof found === 'string')
|
|
42
|
+
return { ok: false, code: found };
|
|
43
|
+
if (found.phase === 'paused')
|
|
44
|
+
return { ok: false, code: 'already-paused' };
|
|
45
|
+
const state = { ...found, phase: 'paused', at: now };
|
|
46
|
+
held = state;
|
|
47
|
+
return { ok: true, state };
|
|
48
|
+
},
|
|
49
|
+
resume(sessionId, now) {
|
|
50
|
+
const found = ownerOf(held, sessionId, now);
|
|
51
|
+
if (typeof found === 'string')
|
|
52
|
+
return { ok: false, code: found };
|
|
53
|
+
if (found.phase === 'debugging')
|
|
54
|
+
return { ok: false, code: 'already-debugging' };
|
|
55
|
+
const state = { ...found, phase: 'debugging', at: now };
|
|
56
|
+
held = state;
|
|
57
|
+
return { ok: true, state };
|
|
58
|
+
},
|
|
59
|
+
stop(sessionId, now) {
|
|
60
|
+
const found = ownerOf(held, sessionId, now);
|
|
61
|
+
if (typeof found === 'string')
|
|
62
|
+
return { ok: false, code: found };
|
|
63
|
+
held = null;
|
|
64
|
+
return { ok: true, state: found };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function debugOwnerText(state) {
|
|
69
|
+
return `会话 ${state.sessionId}(自 ${new Date(state.startedAt).toISOString().slice(11, 19)}Z 起在调试 ${state.unit})`;
|
|
70
|
+
}
|
package/package.json
CHANGED
package/src/api/dsh.ts
CHANGED
|
@@ -151,7 +151,12 @@ export interface DshToolDefinition {
|
|
|
151
151
|
export interface DshToolRunContext {
|
|
152
152
|
readonly agent?:
|
|
153
153
|
| {
|
|
154
|
-
readonly session?:
|
|
154
|
+
readonly session?:
|
|
155
|
+
| {
|
|
156
|
+
readonly id?: string;
|
|
157
|
+
readonly header?: { readonly cwd?: string } | undefined;
|
|
158
|
+
}
|
|
159
|
+
| undefined;
|
|
155
160
|
}
|
|
156
161
|
| undefined;
|
|
157
162
|
}
|
package/src/api/host-write.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { ApiResponse } from './workbench-client.js';
|
|
2
2
|
|
|
3
|
-
export type WriteChannel =
|
|
3
|
+
export type WriteChannel =
|
|
4
|
+
| 'prefs'
|
|
5
|
+
| 'actions'
|
|
6
|
+
| 'provision'
|
|
7
|
+
| 'diy-debug';
|
|
4
8
|
|
|
5
9
|
export function writeRoute(channel: WriteChannel, resource: string): string {
|
|
6
10
|
if (channel === 'provision') return 'provision';
|
|
@@ -18,10 +18,6 @@
|
|
|
18
18
|
row-gap: var(--wb-len-8);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
.wb-formRows > .wb-formRow {
|
|
22
|
-
grid-template-columns: subgrid;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
21
|
.wb-formRow[data-layout='stacked'] {
|
|
26
22
|
grid-template-columns: minmax(0, 1fr);
|
|
27
23
|
align-items: stretch;
|
|
@@ -67,3 +63,13 @@
|
|
|
67
63
|
.wb-formRow:has(:disabled) .wb-formLabel {
|
|
68
64
|
opacity: 0.4;
|
|
69
65
|
}
|
|
66
|
+
|
|
67
|
+
.wb-formRows > .wb-formRow {
|
|
68
|
+
grid-template-columns: subgrid;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@supports not (grid-template-columns: subgrid) {
|
|
72
|
+
.wb-formRows > .wb-formRow {
|
|
73
|
+
grid-template-columns: minmax(72px, max-content) minmax(0, 1fr);
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/components/Rows.css
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
.wb-row:has(> [data-wb-dot]) {
|
|
3
|
-
grid-template-columns: 84px minmax(0, 1fr);
|
|
4
|
-
}
|
|
5
1
|
.wb-rows {
|
|
6
2
|
display: flex;
|
|
7
3
|
flex-direction: column;
|
|
8
4
|
}
|
|
9
5
|
.wb-row {
|
|
10
6
|
display: grid;
|
|
11
|
-
grid-template-columns:
|
|
7
|
+
grid-template-columns: minmax(72px, max-content) minmax(0, 1fr);
|
|
12
8
|
align-items: baseline;
|
|
13
9
|
gap: var(--wb-len-8);
|
|
14
10
|
padding: var(--wb-len-4) 0;
|
|
@@ -17,6 +13,7 @@
|
|
|
17
13
|
border-top: 0.5px solid var(--dsw-alias-border-l2);
|
|
18
14
|
}
|
|
19
15
|
.wb-rowKey {
|
|
16
|
+
white-space: nowrap;
|
|
20
17
|
font-size: var(--dsw-font-xxxs-11-font-size);
|
|
21
18
|
line-height: var(--wb-line-16);
|
|
22
19
|
color: var(--dsw-alias-label-tertiary);
|
|
@@ -28,3 +25,13 @@
|
|
|
28
25
|
color: var(--dsw-alias-label-primary);
|
|
29
26
|
overflow-wrap: anywhere;
|
|
30
27
|
}
|
|
28
|
+
|
|
29
|
+
.wb-rows > .wb-row {
|
|
30
|
+
grid-template-columns: subgrid;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@supports not (grid-template-columns: subgrid) {
|
|
34
|
+
.wb-rows > .wb-row {
|
|
35
|
+
grid-template-columns: minmax(72px, max-content) minmax(0, 1fr);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -61,6 +61,26 @@
|
|
|
61
61
|
box-shadow: none;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
.wb-selectIconOnly {
|
|
65
|
+
width: var(--wb-len-36);
|
|
66
|
+
height: var(--wb-len-36);
|
|
67
|
+
min-height: 0;
|
|
68
|
+
gap: 0;
|
|
69
|
+
padding: 0;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.wb-selectReveal {
|
|
74
|
+
display: inline-flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
justify-content: center;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.wb-selectIconOnly .wb-selectIcon {
|
|
80
|
+
width: 100%;
|
|
81
|
+
height: 100%;
|
|
82
|
+
}
|
|
83
|
+
|
|
64
84
|
.wb-selectAnchor {
|
|
65
85
|
display: block;
|
|
66
86
|
width: 100%;
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
let states: unknown[] = [];
|
|
4
4
|
vi.mock('../lib/react.js', () => ({
|
|
5
|
+
Fragment: Symbol.for('react.fragment'),
|
|
5
6
|
h: (type: unknown, props: unknown, ...children: unknown[]) => ({ type, props, children }),
|
|
6
7
|
useEffect: () => {},
|
|
7
8
|
useMemo: (compute: () => unknown) => compute(),
|
|
@@ -53,6 +54,10 @@ function elementsOf(root: unknown): readonly AnyElement[] {
|
|
|
53
54
|
return collect(root);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
function marked(nodes: readonly AnyElement[]): boolean {
|
|
58
|
+
return nodes.some((one) => one.props?.['data-wb-select-reveal'] === '');
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
function menuOf(root: unknown): AnyElement {
|
|
57
62
|
const menu = elementsOf(root).find((one) => one.type === Menu);
|
|
58
63
|
if (menu === undefined) throw new Error('没有渲染出 Menu');
|
|
@@ -113,6 +118,38 @@ describe('SelectField:工作区那套下拉的公共形状', () => {
|
|
|
113
118
|
expect(inputOf(menu.props['anchor'])?.props['value']).toBe('');
|
|
114
119
|
});
|
|
115
120
|
|
|
121
|
+
it('★ 只显示图标那一档(折叠左栏那个工作区钮):没有值也没有箭头,读屏名字只能来自 ariaLabel', () => {
|
|
122
|
+
const icon = { type: 'i', props: {}, children: [] };
|
|
123
|
+
const trigger = elementsOf(
|
|
124
|
+
menuOf(render({ variant: 'ghost', iconOnly: true, icon, ariaLabel: '切换项目' })).props['anchor'],
|
|
125
|
+
).find((one) => one.type === 'button');
|
|
126
|
+
|
|
127
|
+
expect(trigger?.props['className']).toBe('wb-selectTrigger wb-selectGhost wb-selectIconOnly');
|
|
128
|
+
expect(trigger?.props['aria-label']).toBe('切换项目');
|
|
129
|
+
expect(JSON.stringify(trigger?.children)).not.toContain('wb-selectValue');
|
|
130
|
+
expect(JSON.stringify(trigger?.children)).not.toContain('wb-selectChevron');
|
|
131
|
+
expect(elementsOf(trigger?.children).some((one) => one.props['className'] === 'wb-selectIcon')).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('不给 iconOnly 时一个字节都不变(宽态那套触发器照旧)', () => {
|
|
135
|
+
const trigger = elementsOf(menuOf(render({ variant: 'ghost' })).props['anchor']).find(
|
|
136
|
+
(one) => one.type === 'button',
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(trigger?.props['className']).toBe('wb-selectTrigger wb-selectGhost');
|
|
140
|
+
expect(JSON.stringify(trigger?.children)).toContain('wb-selectValue');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('★ search=false:锚点仍是触发钮(折叠左栏那个下拉要的就是"点开直接是列表")', () => {
|
|
144
|
+
const root = render({ search: false });
|
|
145
|
+
const anchor = elementsOf(menuOf(root).props['anchor']);
|
|
146
|
+
|
|
147
|
+
expect(anchor.some((one) => one.type === 'button')).toBe(true);
|
|
148
|
+
const classes = anchor.map((one) => one.props?.['className']).filter((one) => typeof one === 'string');
|
|
149
|
+
expect(classes).not.toContain('wb-selectSearch');
|
|
150
|
+
expect(inputOf(menuOf(root).props['anchor'])).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
116
153
|
it('禁用:触发钮真的 disabled(压暗交给样式,别只在视觉上假装)', () => {
|
|
117
154
|
const root = render({ disabled: true });
|
|
118
155
|
const trigger = elementsOf(menuOf(root).props['anchor']).find((one) => one.type === 'button');
|
|
@@ -239,6 +276,32 @@ describe('SelectField:工作区那套下拉的公共形状', () => {
|
|
|
239
276
|
expect(defaultSelectFilter(option, 'nope')).toBe(false);
|
|
240
277
|
});
|
|
241
278
|
|
|
279
|
+
it('★ revealId:只给那一行的图标裹上标记(开列表时靠它滚进可视区),别行一个字节不变', () => {
|
|
280
|
+
const items = selectEntries(
|
|
281
|
+
[
|
|
282
|
+
{ value: 'a', label: '甲', icon: <span>甲</span> },
|
|
283
|
+
{ value: 'b', label: '乙', icon: <span>乙</span> },
|
|
284
|
+
],
|
|
285
|
+
undefined,
|
|
286
|
+
'b',
|
|
287
|
+
);
|
|
288
|
+
const iconOf = (id: string): unknown =>
|
|
289
|
+
(items.find((one) => (one as { id?: string }).id === id) as { icon?: unknown } | undefined)?.icon;
|
|
290
|
+
|
|
291
|
+
expect(marked(elementsOf(iconOf('a')))).toBe(false);
|
|
292
|
+
expect(marked(elementsOf(iconOf('b')))).toBe(true);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('★ revealId 撞上没有图标的行:退而标记它的文字(标记只落一处)', () => {
|
|
296
|
+
const items = selectEntries([{ value: 'a', label: '甲' }], undefined, 'a');
|
|
297
|
+
const row = items.find((one) => (one as { id?: string }).id === 'a') as
|
|
298
|
+
| { label?: unknown; icon?: unknown }
|
|
299
|
+
| undefined;
|
|
300
|
+
|
|
301
|
+
expect(row?.icon).toBeUndefined();
|
|
302
|
+
expect(marked(elementsOf(row?.label))).toBe(true);
|
|
303
|
+
});
|
|
304
|
+
|
|
242
305
|
it('条目构造:图标与禁用照传,分组标题只在换组时出现一次', () => {
|
|
243
306
|
const items = selectEntries(
|
|
244
307
|
[
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import type { KeyboardEvent as ReactKeyboardEvent, ReactNode, RefObject } from 'react';
|
|
2
2
|
import type { MenuEntry, MenuItem } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
3
3
|
import { h, useEffect, useMemo, useRef, useState } from '../lib/react.js';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
IconSearchOutlineRegular,
|
|
7
|
-
Menu,
|
|
8
|
-
TextShimmer,
|
|
9
|
-
} from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
|
+
import { IconSearchOutlineRegular, Menu } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
5
|
+
import { selectLoadingNode, selectTriggerNode } from './SelectFieldNodes.js';
|
|
10
6
|
import { Field } from './Field.js';
|
|
11
7
|
import { TextField } from './TextField.js';
|
|
12
|
-
import { Spinner } from './Spinner.js';
|
|
13
8
|
import './SelectField.css';
|
|
14
9
|
|
|
15
10
|
export interface SelectFieldOption {
|
|
@@ -56,6 +51,8 @@ export interface SelectFieldProps {
|
|
|
56
51
|
readonly onFooter?: (() => void) | undefined;
|
|
57
52
|
readonly selection?: 'check' | 'fill' | undefined;
|
|
58
53
|
readonly variant?: 'ghost' | 'boxed' | undefined;
|
|
54
|
+
readonly iconOnly?: boolean | undefined;
|
|
55
|
+
readonly revealId?: string | undefined;
|
|
59
56
|
readonly ariaLabel?: string | undefined;
|
|
60
57
|
readonly title?: string | undefined;
|
|
61
58
|
readonly id?: string | undefined;
|
|
@@ -71,75 +68,54 @@ function mergeClass(...parts: readonly (string | undefined)[]): string {
|
|
|
71
68
|
return parts.filter((one) => one !== undefined && one !== '').join(' ');
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
function
|
|
71
|
+
function triggerClassOf(variantClass: string | undefined, iconOnly: boolean): string {
|
|
72
|
+
return mergeClass('wb-selectTrigger', variantClass, iconOnly ? 'wb-selectIconOnly' : undefined);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function itemOf(option: SelectFieldOption, revealId: string | undefined): MenuItem {
|
|
76
|
+
const mark = option.value === revealId;
|
|
77
|
+
if (!mark) {
|
|
78
|
+
return {
|
|
79
|
+
id: option.value,
|
|
80
|
+
label: option.label,
|
|
81
|
+
...(option.icon === undefined ? {} : { icon: option.icon }),
|
|
82
|
+
...(option.disabled === true ? { disabled: true } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const revealed = (node: ReactNode): ReactNode => (
|
|
86
|
+
<span className="wb-selectReveal" data-wb-select-reveal="">
|
|
87
|
+
{node}
|
|
88
|
+
</span>
|
|
89
|
+
);
|
|
75
90
|
return {
|
|
76
91
|
id: option.value,
|
|
77
|
-
label: option.label,
|
|
78
|
-
...(option.icon === undefined ? {} : { icon: option.icon }),
|
|
92
|
+
label: option.icon === undefined ? revealed(option.label) : option.label,
|
|
93
|
+
...(option.icon === undefined ? {} : { icon: revealed(option.icon) }),
|
|
79
94
|
...(option.disabled === true ? { disabled: true } : {}),
|
|
80
95
|
};
|
|
81
96
|
}
|
|
82
97
|
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
readonly disabled: boolean;
|
|
87
|
-
readonly invalid: boolean;
|
|
88
|
-
readonly open: boolean;
|
|
89
|
-
readonly ariaLabel: string | undefined;
|
|
90
|
-
readonly title: string | undefined;
|
|
91
|
-
readonly icon: ReactNode | undefined;
|
|
92
|
-
readonly value: ReactNode;
|
|
93
|
-
readonly onOpen: () => void;
|
|
94
|
-
}): ReactNode {
|
|
95
|
-
return (
|
|
96
|
-
<button
|
|
97
|
-
type="button"
|
|
98
|
-
id={args.id}
|
|
99
|
-
className={args.className}
|
|
100
|
-
disabled={args.disabled}
|
|
101
|
-
aria-haspopup="listbox"
|
|
102
|
-
aria-expanded={args.open}
|
|
103
|
-
aria-invalid={args.invalid || undefined}
|
|
104
|
-
aria-label={args.ariaLabel}
|
|
105
|
-
title={args.title}
|
|
106
|
-
onClick={args.onOpen}
|
|
107
|
-
>
|
|
108
|
-
{args.icon === undefined ? null : <span className="wb-selectIcon">{args.icon}</span>}
|
|
109
|
-
<span className="wb-selectValue">{args.value}</span>
|
|
110
|
-
<span className="wb-selectChevron">
|
|
111
|
-
<IconChevronDownOutlineRegular size={14} />
|
|
112
|
-
</span>
|
|
113
|
-
</button>
|
|
114
|
-
);
|
|
98
|
+
function revealNode(): HTMLElement | null {
|
|
99
|
+
const found = document.querySelector('[data-wb-select-reveal]');
|
|
100
|
+
return found instanceof HTMLElement ? found : null;
|
|
115
101
|
}
|
|
116
102
|
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
title={args.loadingText}
|
|
134
|
-
>
|
|
135
|
-
<span className="wb-selectValue">
|
|
136
|
-
<TextShimmer active>{args.loadingText ?? '…'}</TextShimmer>
|
|
137
|
-
</span>
|
|
138
|
-
<span className="wb-selectChevron">
|
|
139
|
-
<Spinner size={14} />
|
|
140
|
-
</span>
|
|
141
|
-
</button>
|
|
142
|
-
);
|
|
103
|
+
function useRevealOnOpen(open: boolean, revealId: string | undefined): void {
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!open || revealId === undefined) return;
|
|
106
|
+
revealNode()?.scrollIntoView({ block: 'nearest' });
|
|
107
|
+
}, [open, revealId]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function selectValueNode(
|
|
111
|
+
current: SelectFieldOption | undefined,
|
|
112
|
+
renderValue: SelectFieldProps['renderValue'],
|
|
113
|
+
placeholder: ReactNode,
|
|
114
|
+
): ReactNode {
|
|
115
|
+
if (current === undefined) {
|
|
116
|
+
return <span className="wb-selectPlaceholder">{placeholder ?? '—'}</span>;
|
|
117
|
+
}
|
|
118
|
+
return renderValue === undefined ? current.label : renderValue(current);
|
|
143
119
|
}
|
|
144
120
|
|
|
145
121
|
function searchNode(args: {
|
|
@@ -269,6 +245,7 @@ export function stepSelectCursor(
|
|
|
269
245
|
export function selectEntries(
|
|
270
246
|
shown: readonly SelectFieldOption[],
|
|
271
247
|
emptyText: string | undefined,
|
|
248
|
+
revealId?: string | undefined,
|
|
272
249
|
): readonly MenuEntry[] {
|
|
273
250
|
if (shown.length === 0) {
|
|
274
251
|
return emptyText === undefined ? [] : [{ type: 'label', id: EMPTY_ID, text: emptyText }];
|
|
@@ -280,7 +257,7 @@ export function selectEntries(
|
|
|
280
257
|
out.push({ type: 'label', id: `__wb-select-group-${String(out.length)}`, text: option.group });
|
|
281
258
|
group = option.group;
|
|
282
259
|
}
|
|
283
|
-
out.push(itemOf(option));
|
|
260
|
+
out.push(itemOf(option, revealId));
|
|
284
261
|
}
|
|
285
262
|
return out;
|
|
286
263
|
}
|
|
@@ -368,6 +345,8 @@ export function SelectField(props: SelectFieldProps) {
|
|
|
368
345
|
fallback,
|
|
369
346
|
selection = 'fill',
|
|
370
347
|
variant = 'boxed',
|
|
348
|
+
iconOnly = false,
|
|
349
|
+
revealId,
|
|
371
350
|
ariaLabel,
|
|
372
351
|
title,
|
|
373
352
|
id,
|
|
@@ -377,15 +356,17 @@ export function SelectField(props: SelectFieldProps) {
|
|
|
377
356
|
} = props;
|
|
378
357
|
const state = useSelectState(props);
|
|
379
358
|
const current = options.find((option) => option.value === value);
|
|
359
|
+
|
|
360
|
+
useRevealOnOpen(state.open, revealId);
|
|
380
361
|
const variantClass = variant === 'ghost' ? 'wb-selectGhost' : undefined;
|
|
381
|
-
const triggerClass =
|
|
362
|
+
const triggerClass = triggerClassOf(variantClass, iconOnly);
|
|
382
363
|
|
|
383
364
|
if (loading) {
|
|
384
365
|
return framed(
|
|
385
366
|
label,
|
|
386
367
|
hint,
|
|
387
368
|
className,
|
|
388
|
-
|
|
369
|
+
selectLoadingNode({ id, className: triggerClass, invalid, ariaLabel, loadingText }),
|
|
389
370
|
);
|
|
390
371
|
}
|
|
391
372
|
|
|
@@ -406,7 +387,7 @@ export function SelectField(props: SelectFieldProps) {
|
|
|
406
387
|
);
|
|
407
388
|
}
|
|
408
389
|
|
|
409
|
-
const trigger =
|
|
390
|
+
const trigger = selectTriggerNode({
|
|
410
391
|
id,
|
|
411
392
|
className: triggerClass,
|
|
412
393
|
disabled,
|
|
@@ -415,14 +396,8 @@ export function SelectField(props: SelectFieldProps) {
|
|
|
415
396
|
ariaLabel,
|
|
416
397
|
title,
|
|
417
398
|
icon,
|
|
418
|
-
value:
|
|
419
|
-
|
|
420
|
-
<span className="wb-selectPlaceholder">{placeholder ?? '—'}</span>
|
|
421
|
-
) : renderValue === undefined ? (
|
|
422
|
-
current.label
|
|
423
|
-
) : (
|
|
424
|
-
renderValue(current)
|
|
425
|
-
),
|
|
399
|
+
value: selectValueNode(current, renderValue, placeholder),
|
|
400
|
+
iconOnly,
|
|
426
401
|
onOpen: () => state.setOpen(true),
|
|
427
402
|
});
|
|
428
403
|
|
|
@@ -469,7 +444,7 @@ export function SelectField(props: SelectFieldProps) {
|
|
|
469
444
|
anchor,
|
|
470
445
|
selection,
|
|
471
446
|
selectedId: selectCursorValue(state.shown, state.cursor) ?? undefined,
|
|
472
|
-
items: selectEntries(state.shown, state.empty),
|
|
447
|
+
items: selectEntries(state.shown, state.empty, revealId),
|
|
473
448
|
footer:
|
|
474
449
|
footer === undefined
|
|
475
450
|
? undefined
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Fragment, h } from '../lib/react.js';
|
|
3
|
+
import {
|
|
4
|
+
IconChevronDownOutlineRegular,
|
|
5
|
+
TextShimmer,
|
|
6
|
+
} from '@deepseek-ai/dsh-client-ui-primitives';
|
|
7
|
+
import { Spinner } from './Spinner.js';
|
|
8
|
+
|
|
9
|
+
export function selectTriggerNode(args: {
|
|
10
|
+
readonly id: string | undefined;
|
|
11
|
+
readonly className: string;
|
|
12
|
+
readonly disabled: boolean;
|
|
13
|
+
readonly invalid: boolean;
|
|
14
|
+
readonly open: boolean;
|
|
15
|
+
readonly ariaLabel: string | undefined;
|
|
16
|
+
readonly title: string | undefined;
|
|
17
|
+
readonly icon: ReactNode | undefined;
|
|
18
|
+
readonly value: ReactNode;
|
|
19
|
+
readonly iconOnly: boolean;
|
|
20
|
+
readonly onOpen: () => void;
|
|
21
|
+
}): ReactNode {
|
|
22
|
+
return (
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
id={args.id}
|
|
26
|
+
className={args.className}
|
|
27
|
+
disabled={args.disabled}
|
|
28
|
+
aria-haspopup="listbox"
|
|
29
|
+
aria-expanded={args.open}
|
|
30
|
+
aria-invalid={args.invalid || undefined}
|
|
31
|
+
aria-label={args.ariaLabel}
|
|
32
|
+
title={args.title}
|
|
33
|
+
onClick={args.onOpen}
|
|
34
|
+
>
|
|
35
|
+
{args.iconOnly ? (
|
|
36
|
+
<span className="wb-selectIcon">{args.icon}</span>
|
|
37
|
+
) : (
|
|
38
|
+
<Fragment>
|
|
39
|
+
{args.icon === undefined ? null : <span className="wb-selectIcon">{args.icon}</span>}
|
|
40
|
+
<span className="wb-selectValue">{args.value}</span>
|
|
41
|
+
<span className="wb-selectChevron">
|
|
42
|
+
<IconChevronDownOutlineRegular size={14} />
|
|
43
|
+
</span>
|
|
44
|
+
</Fragment>
|
|
45
|
+
)}
|
|
46
|
+
</button>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function selectLoadingNode(args: {
|
|
51
|
+
readonly id: string | undefined;
|
|
52
|
+
readonly className: string;
|
|
53
|
+
readonly invalid: boolean;
|
|
54
|
+
readonly ariaLabel: string | undefined;
|
|
55
|
+
readonly loadingText: string | undefined;
|
|
56
|
+
}): ReactNode {
|
|
57
|
+
return (
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
id={args.id}
|
|
61
|
+
className={`${args.className} wb-selectLoading`}
|
|
62
|
+
disabled
|
|
63
|
+
aria-busy="true"
|
|
64
|
+
aria-invalid={args.invalid || undefined}
|
|
65
|
+
aria-label={args.ariaLabel}
|
|
66
|
+
title={args.loadingText}
|
|
67
|
+
>
|
|
68
|
+
<span className="wb-selectValue">
|
|
69
|
+
<TextShimmer active>{args.loadingText ?? '…'}</TextShimmer>
|
|
70
|
+
</span>
|
|
71
|
+
<span className="wb-selectChevron">
|
|
72
|
+
<Spinner size={14} />
|
|
73
|
+
</span>
|
|
74
|
+
</button>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
package/src/config/constants.ts
CHANGED
|
@@ -16,6 +16,9 @@ export const PROVISION_PREFIX = `${API_PREFIX}/provision`;
|
|
|
16
16
|
|
|
17
17
|
export const PANEL_STATE_PATH = "panel-state";
|
|
18
18
|
|
|
19
|
+
export const DIY_DEBUG_PATH = "diy-debug";
|
|
20
|
+
export const DIY_DEBUG_ACT_PATH = `${DIY_DEBUG_PATH}/act`;
|
|
21
|
+
|
|
19
22
|
export const PANEL_STATE_PREFIX = `${API_PREFIX}/${PANEL_STATE_PATH}`;
|
|
20
23
|
|
|
21
24
|
export const OPENCLI_PIN = "1.8.8";
|
|
@@ -28,6 +31,8 @@ export const UNITS_SERVICE = "workbenchUnits";
|
|
|
28
31
|
|
|
29
32
|
export const PANEL_STATE_SERVICE = "workbenchPanelState";
|
|
30
33
|
|
|
34
|
+
export const DIY_DEBUG_SERVICE = "workbenchDiyDebug";
|
|
35
|
+
|
|
31
36
|
export const PANEL_LIST_LIMIT = 50;
|
|
32
37
|
|
|
33
38
|
export const OPENCLI_TIMEOUT_MS = 120_000;
|
package/src/host/http.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DshHostContext, DshRequest, DshResponse } from '../api/dsh.js';
|
|
2
2
|
import { unitFactScope } from '../api/facts.js';
|
|
3
3
|
import { parseCardsReport, parseCrashReport, type PanelStateStore } from '../lib/panel-state.js';
|
|
4
|
-
import { ACTIONS_PREFIX, API_PREFIX, PANEL_STATE_PREFIX, PLUGIN_ID, PREFS_PREFIX, PROVISION_PREFIX } from '../config/constants.js';
|
|
4
|
+
import { ACTIONS_PREFIX, API_PREFIX, DIY_DEBUG_PATH, PANEL_STATE_PREFIX, PLUGIN_ID, PREFS_PREFIX, PROVISION_PREFIX } from '../config/constants.js';
|
|
5
5
|
import {
|
|
6
6
|
parseProvisionBody,
|
|
7
7
|
type ProvisionHandler,
|
|
@@ -300,8 +300,7 @@ export function registerActions(
|
|
|
300
300
|
});
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
export function registerPanelState(ctx: DshHostContext, store: PanelStateStore): () => void {
|
|
304
|
-
return registerPost(ctx, {
|
|
303
|
+
export function registerPanelState(ctx: DshHostContext, store: PanelStateStore): () => void { return registerPost(ctx, {
|
|
305
304
|
prefix: PANEL_STATE_PREFIX,
|
|
306
305
|
what: '面板运行态',
|
|
307
306
|
refusedCode: 'report-refused',
|
|
@@ -327,3 +326,17 @@ export function registerPanelState(ctx: DshHostContext, store: PanelStateStore):
|
|
|
327
326
|
],
|
|
328
327
|
});
|
|
329
328
|
}
|
|
329
|
+
|
|
330
|
+
export function registerDiyDebugAct(
|
|
331
|
+
ctx: DshHostContext,
|
|
332
|
+
resource: PostResource,
|
|
333
|
+
log?: { bump(facts: readonly string[]): unknown } | undefined,
|
|
334
|
+
): () => void {
|
|
335
|
+
return registerPost(ctx, {
|
|
336
|
+
prefix: `${API_PREFIX}/${DIY_DEBUG_PATH}`,
|
|
337
|
+
what: '工作台 DIY 调试动作',
|
|
338
|
+
refusedCode: 'action-refused',
|
|
339
|
+
resources: [resource],
|
|
340
|
+
wrote: () => log?.bump(['diy-debug']),
|
|
341
|
+
});
|
|
342
|
+
}
|