@shendeguize/dsh-agent-sidecar 0.1.0 → 0.2.0
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/README.md +78 -11
- package/lib/client.js +4708 -2932
- package/lib/client.js.map +1 -1
- package/package.json +3 -1
- package/src/client/analysis/AnalysisPanel.tsx +19 -27
- package/src/client/analysis/analysis.module.css +36 -97
- package/src/client/board/Board.tsx +255 -48
- package/src/client/board/board.module.css +113 -92
- package/src/client/board/logic.ts +99 -21
- package/src/client/board/project-view-logic.ts +27 -34
- package/src/client/board/project-view.module.css +46 -83
- package/src/client/board/project-view.tsx +50 -8
- package/src/client/board/strings.ts +70 -85
- package/src/client/commands.ts +30 -15
- package/src/client/controller.ts +27 -7
- package/src/client/detail/SessionDetail.tsx +234 -37
- package/src/client/detail/detail.module.css +100 -153
- package/src/client/detail/logic.ts +220 -29
- package/src/client/detail/strings.ts +66 -77
- package/src/client/detail-glue.ts +33 -0
- package/src/client/detail-view.module.css +43 -35
- package/src/client/detail-view.tsx +138 -118
- package/src/client/dsh-tools/LineageTree.tsx +22 -13
- package/src/client/dsh-tools/SearchPanel.tsx +18 -11
- package/src/client/dsh-tools/dsh-tools.module.css +53 -140
- package/src/client/dsh-tools/strings.ts +42 -77
- package/src/client/index.ts +285 -124
- package/src/client/inject/InjectPanel.tsx +81 -61
- package/src/client/inject/inject.module.css +97 -197
- package/src/client/inject/logic.ts +38 -0
- package/src/client/lifecycle/handoff.ts +83 -0
- package/src/client/locales/en.ts +91 -4
- package/src/client/locales/host.ts +131 -0
- package/src/client/locales/index.ts +196 -8
- package/src/client/locales/react.ts +10 -0
- package/src/client/locales/view.ts +38 -0
- package/src/client/locales/zh.ts +200 -125
- package/src/client/mount.tsx +75 -41
- package/src/client/navigation/CenterOverlay.tsx +40 -0
- package/src/client/navigation/center-overlay.module.css +51 -0
- package/src/client/navigation/center.ts +45 -0
- package/src/client/navigation/modal-isolation.ts +152 -0
- package/src/client/navigation/modal-surface-anchor.ts +72 -0
- package/src/client/navigation/sidebar-entry.module.css +73 -0
- package/src/client/navigation/sidebar-entry.ts +309 -0
- package/src/client/primitives/StaticPill.tsx +21 -0
- package/src/client/settings-card.module.css +35 -119
- package/src/client/settings-card.tsx +31 -153
- package/src/client/settings-fields.tsx +131 -0
- package/src/client/sidebar/SidebarTab.tsx +156 -0
- package/src/client/sidebar/model.ts +65 -0
- package/src/client/sidebar/sidebar-tab.module.css +118 -0
- package/src/client/sidebar-tab.tsx +46 -320
- package/src/client/theme/agsc.module.css +31 -0
- package/src/client/theme/parts.ts +56 -0
- package/src/client/ui-integration.ts +86 -0
- package/src/client/widget.tsx +42 -16
- package/src/client/inject/overlay.module.css +0 -22
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Board styles (design §5.4): every color rides a --dsw-alias-* design
|
|
3
|
-
* token with a neutral fallback — no theme colors are hardcoded as the
|
|
4
|
-
* only value. Layout follows the dsh-project-kanban blueprint (flex top
|
|
5
|
-
* bar, grouped sections, auto-fill card grid).
|
|
6
|
-
*/
|
|
1
|
+
/* Board layout consumes the public Agent Sidecar theme contract. */
|
|
7
2
|
|
|
8
3
|
.root {
|
|
9
4
|
display: flex;
|
|
@@ -14,8 +9,8 @@
|
|
|
14
9
|
padding: 16px 20px;
|
|
15
10
|
box-sizing: border-box;
|
|
16
11
|
overflow-y: auto;
|
|
17
|
-
color: var(--
|
|
18
|
-
background: var(--
|
|
12
|
+
color: var(--agsc-fg);
|
|
13
|
+
background: var(--agsc-bg);
|
|
19
14
|
}
|
|
20
15
|
|
|
21
16
|
/* ------------------------------------------------------------- top bar */
|
|
@@ -30,50 +25,36 @@
|
|
|
30
25
|
.title {
|
|
31
26
|
font-size: 15px;
|
|
32
27
|
font-weight: 650;
|
|
33
|
-
color: var(--
|
|
28
|
+
color: var(--agsc-fg);
|
|
34
29
|
margin-right: 2px;
|
|
35
30
|
}
|
|
36
31
|
|
|
37
|
-
.badge {
|
|
38
|
-
display: inline-flex;
|
|
39
|
-
align-items: center;
|
|
40
|
-
gap: 5px;
|
|
41
|
-
font-size: 12px;
|
|
42
|
-
line-height: 20px;
|
|
43
|
-
padding: 0 8px;
|
|
44
|
-
border-radius: 999px;
|
|
45
|
-
border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.08));
|
|
46
|
-
background: var(--dsw-alias-bg-layer-1, rgba(0, 0, 0, 0.03));
|
|
47
|
-
color: var(--dsw-alias-label-secondary, #57606a);
|
|
48
|
-
white-space: nowrap;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
32
|
.dot {
|
|
52
33
|
width: 8px;
|
|
53
34
|
height: 8px;
|
|
54
35
|
border-radius: 50%;
|
|
55
36
|
flex: none;
|
|
56
|
-
background: var(--dsw-alias-label-tertiary
|
|
37
|
+
background: var(--dsw-alias-label-tertiary);
|
|
57
38
|
}
|
|
58
39
|
|
|
59
|
-
.dot[data-tone='
|
|
60
|
-
background: var(--dsw-alias-
|
|
40
|
+
.dot[data-tone='neutral'] {
|
|
41
|
+
background: var(--dsw-alias-label-tertiary);
|
|
61
42
|
}
|
|
62
43
|
|
|
63
|
-
.dot[data-tone='
|
|
64
|
-
background: var(--
|
|
44
|
+
.dot[data-tone='muted'] {
|
|
45
|
+
background: var(--agsc-fg-dimmed);
|
|
65
46
|
}
|
|
66
47
|
|
|
67
|
-
|
|
68
|
-
background: var(--dsw-alias-state-error-primary, #cf222e);
|
|
69
|
-
}
|
|
48
|
+
/* UX-01: clickable working/waiting count badges + the total counter. */
|
|
70
49
|
|
|
71
|
-
.
|
|
72
|
-
|
|
50
|
+
.countBadge {
|
|
51
|
+
white-space: nowrap;
|
|
73
52
|
}
|
|
74
53
|
|
|
75
|
-
.
|
|
76
|
-
|
|
54
|
+
.countTotal {
|
|
55
|
+
font-size: 12px;
|
|
56
|
+
color: var(--agsc-fg-secondary);
|
|
57
|
+
white-space: nowrap;
|
|
77
58
|
}
|
|
78
59
|
|
|
79
60
|
.spacer {
|
|
@@ -85,62 +66,52 @@
|
|
|
85
66
|
align-items: center;
|
|
86
67
|
gap: 5px;
|
|
87
68
|
font-size: 12px;
|
|
88
|
-
color: var(--
|
|
69
|
+
color: var(--agsc-fg-secondary);
|
|
89
70
|
white-space: nowrap;
|
|
90
71
|
}
|
|
91
72
|
|
|
92
73
|
.select {
|
|
74
|
+
height: 28px;
|
|
93
75
|
font-size: 12px;
|
|
94
76
|
font-family: inherit;
|
|
95
|
-
color: var(--
|
|
96
|
-
background: var(--
|
|
97
|
-
border: 1px solid var(--
|
|
98
|
-
border-radius:
|
|
99
|
-
padding:
|
|
77
|
+
color: var(--agsc-fg);
|
|
78
|
+
background: var(--agsc-bg-raised);
|
|
79
|
+
border: 1px solid var(--agsc-border-strong);
|
|
80
|
+
border-radius: 14px;
|
|
81
|
+
padding: 0 10px;
|
|
100
82
|
}
|
|
101
83
|
|
|
102
84
|
.checkbox {
|
|
103
|
-
accent-color: var(--
|
|
85
|
+
accent-color: var(--agsc-accent);
|
|
104
86
|
margin: 0;
|
|
105
87
|
}
|
|
106
88
|
|
|
107
|
-
.refresh {
|
|
108
|
-
font-size: 12px;
|
|
109
|
-
font-family: inherit;
|
|
110
|
-
color: var(--dsw-alias-label-secondary, #57606a);
|
|
111
|
-
background: transparent;
|
|
112
|
-
border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.08));
|
|
113
|
-
border-radius: 6px;
|
|
114
|
-
padding: 2px 10px;
|
|
115
|
-
cursor: pointer;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.refresh:hover {
|
|
119
|
-
color: var(--dsw-alias-label-primary, #1f2328);
|
|
120
|
-
border-color: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
|
|
121
|
-
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
89
|
/* -------------------------------------------------------------- banner */
|
|
125
90
|
|
|
126
91
|
.banner {
|
|
127
92
|
font-size: 12px;
|
|
128
93
|
line-height: 18px;
|
|
129
94
|
padding: 6px 10px;
|
|
130
|
-
border-radius:
|
|
131
|
-
border: 1px solid var(--
|
|
95
|
+
border-radius: var(--agsc-radius-control);
|
|
96
|
+
border: 1px solid var(--agsc-border);
|
|
132
97
|
}
|
|
133
98
|
|
|
134
99
|
.banner[data-tone='warn'] {
|
|
135
|
-
color: var(--
|
|
136
|
-
background: var(--dsw-alias-state-warn-tertiary
|
|
137
|
-
border-color: var(--dsw-alias-state-warn-secondary
|
|
100
|
+
color: var(--agsc-warn);
|
|
101
|
+
background: var(--dsw-alias-state-warn-tertiary);
|
|
102
|
+
border-color: var(--dsw-alias-state-warn-secondary);
|
|
138
103
|
}
|
|
139
104
|
|
|
140
105
|
.banner[data-tone='danger'] {
|
|
141
|
-
color: var(--
|
|
142
|
-
background: var(--dsw-alias-state-error-secondary
|
|
143
|
-
border-color: var(--dsw-alias-state-error-secondary
|
|
106
|
+
color: var(--agsc-err);
|
|
107
|
+
background: var(--dsw-alias-state-error-secondary);
|
|
108
|
+
border-color: var(--dsw-alias-state-error-secondary);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.bannerDismiss {
|
|
112
|
+
margin-left: 10px;
|
|
113
|
+
color: inherit;
|
|
114
|
+
text-decoration: underline;
|
|
144
115
|
}
|
|
145
116
|
|
|
146
117
|
/* -------------------------------------------------------- empty states */
|
|
@@ -160,14 +131,14 @@
|
|
|
160
131
|
.emptyTitle {
|
|
161
132
|
font-size: 14px;
|
|
162
133
|
font-weight: 600;
|
|
163
|
-
color: var(--
|
|
134
|
+
color: var(--agsc-fg);
|
|
164
135
|
}
|
|
165
136
|
|
|
166
137
|
.emptyHint {
|
|
167
138
|
font-size: 12px;
|
|
168
139
|
max-width: 420px;
|
|
169
140
|
line-height: 20px;
|
|
170
|
-
color: var(--
|
|
141
|
+
color: var(--agsc-fg-secondary);
|
|
171
142
|
}
|
|
172
143
|
|
|
173
144
|
/* -------------------------------------------------------- group blocks */
|
|
@@ -178,17 +149,41 @@
|
|
|
178
149
|
gap: 8px;
|
|
179
150
|
}
|
|
180
151
|
|
|
152
|
+
/* Collapsible group header (UX-02): a full-width transparent button. */
|
|
181
153
|
.groupHead {
|
|
182
154
|
display: flex;
|
|
183
155
|
align-items: baseline;
|
|
184
156
|
gap: 8px;
|
|
185
157
|
min-width: 0;
|
|
158
|
+
width: 100%;
|
|
159
|
+
padding: 0;
|
|
160
|
+
border: none;
|
|
161
|
+
background: transparent;
|
|
162
|
+
font-family: inherit;
|
|
163
|
+
text-align: left;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.chevron {
|
|
168
|
+
flex: none;
|
|
169
|
+
font-size: 10px;
|
|
170
|
+
color: var(--agsc-fg-dimmed);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.groupAttention {
|
|
174
|
+
flex: none;
|
|
175
|
+
font-size: 11px;
|
|
176
|
+
color: var(--agsc-warn);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.showMore {
|
|
180
|
+
align-self: flex-start;
|
|
186
181
|
}
|
|
187
182
|
|
|
188
183
|
.groupName {
|
|
189
184
|
font-size: 13px;
|
|
190
185
|
font-weight: 600;
|
|
191
|
-
color: var(--
|
|
186
|
+
color: var(--agsc-fg);
|
|
192
187
|
overflow: hidden;
|
|
193
188
|
text-overflow: ellipsis;
|
|
194
189
|
white-space: nowrap;
|
|
@@ -197,8 +192,8 @@
|
|
|
197
192
|
.groupCount {
|
|
198
193
|
font-size: 11px;
|
|
199
194
|
flex: none;
|
|
200
|
-
color: var(--
|
|
201
|
-
background: var(--dsw-alias-bg-layer-2
|
|
195
|
+
color: var(--agsc-fg-secondary);
|
|
196
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
202
197
|
border-radius: 999px;
|
|
203
198
|
padding: 0 8px;
|
|
204
199
|
line-height: 18px;
|
|
@@ -216,24 +211,37 @@
|
|
|
216
211
|
display: flex;
|
|
217
212
|
flex-direction: column;
|
|
218
213
|
gap: 6px;
|
|
219
|
-
text-align: left;
|
|
220
|
-
font-family: inherit;
|
|
221
214
|
min-width: 0;
|
|
222
215
|
padding: 10px 12px;
|
|
223
|
-
border-radius:
|
|
224
|
-
border: 1px solid var(--
|
|
225
|
-
background: var(--
|
|
226
|
-
cursor: pointer;
|
|
216
|
+
border-radius: var(--agsc-radius-card);
|
|
217
|
+
border: 1px solid var(--agsc-border-strong);
|
|
218
|
+
background: var(--agsc-bg-raised);
|
|
227
219
|
}
|
|
228
220
|
|
|
229
221
|
.card:hover {
|
|
230
|
-
border-color: var(--dsw-alias-
|
|
231
|
-
background: var(--dsw-alias-interactive-bg-hover
|
|
222
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
223
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.cardOpen {
|
|
227
|
+
display: flex;
|
|
228
|
+
flex-direction: column;
|
|
229
|
+
gap: 6px;
|
|
230
|
+
width: 100%;
|
|
231
|
+
min-width: 0;
|
|
232
|
+
padding: 0;
|
|
233
|
+
border: 0;
|
|
234
|
+
border-radius: var(--agsc-radius-control);
|
|
235
|
+
color: inherit;
|
|
236
|
+
background: none;
|
|
237
|
+
font: inherit;
|
|
238
|
+
text-align: left;
|
|
239
|
+
cursor: pointer;
|
|
232
240
|
}
|
|
233
241
|
|
|
234
|
-
.
|
|
235
|
-
outline: 2px solid var(--
|
|
236
|
-
outline-offset:
|
|
242
|
+
.cardOpen:focus-visible {
|
|
243
|
+
outline: 2px solid var(--agsc-accent);
|
|
244
|
+
outline-offset: 2px;
|
|
237
245
|
}
|
|
238
246
|
|
|
239
247
|
.cardHead {
|
|
@@ -254,43 +262,56 @@
|
|
|
254
262
|
overflow: hidden;
|
|
255
263
|
text-overflow: ellipsis;
|
|
256
264
|
white-space: nowrap;
|
|
257
|
-
color: var(--
|
|
265
|
+
color: var(--agsc-fg);
|
|
258
266
|
}
|
|
259
267
|
|
|
260
268
|
.glyph {
|
|
261
269
|
flex: none;
|
|
262
|
-
color: var(--
|
|
270
|
+
color: var(--agsc-accent);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.statusPill {
|
|
274
|
+
max-width: 100%;
|
|
263
275
|
}
|
|
264
276
|
|
|
265
277
|
.attention {
|
|
266
278
|
font-size: 11px;
|
|
267
|
-
color: var(--
|
|
279
|
+
color: var(--agsc-warn);
|
|
268
280
|
}
|
|
269
281
|
|
|
270
282
|
.attention[data-kind='gap'] {
|
|
271
|
-
color: var(--
|
|
283
|
+
color: var(--agsc-err);
|
|
272
284
|
}
|
|
273
285
|
|
|
274
286
|
.cardTitle {
|
|
275
287
|
font-size: 13px;
|
|
276
|
-
color: var(--
|
|
288
|
+
color: var(--agsc-fg);
|
|
277
289
|
overflow: hidden;
|
|
278
290
|
text-overflow: ellipsis;
|
|
279
291
|
white-space: nowrap;
|
|
280
292
|
}
|
|
281
293
|
|
|
282
294
|
.cardId {
|
|
295
|
+
align-self: flex-start;
|
|
296
|
+
max-width: 100%;
|
|
297
|
+
justify-content: flex-start;
|
|
283
298
|
font-size: 11px;
|
|
284
|
-
font-family:
|
|
285
|
-
color: var(--
|
|
299
|
+
font-family: var(--agsc-font-mono);
|
|
300
|
+
color: var(--agsc-fg-secondary);
|
|
286
301
|
overflow: hidden;
|
|
287
302
|
text-overflow: ellipsis;
|
|
288
303
|
white-space: nowrap;
|
|
289
304
|
}
|
|
290
305
|
|
|
306
|
+
.copied {
|
|
307
|
+
margin-left: 6px;
|
|
308
|
+
font-family: inherit;
|
|
309
|
+
color: var(--agsc-ok);
|
|
310
|
+
}
|
|
311
|
+
|
|
291
312
|
.cardEvent {
|
|
292
313
|
font-size: 12px;
|
|
293
|
-
color: var(--
|
|
314
|
+
color: var(--agsc-fg-secondary);
|
|
294
315
|
overflow: hidden;
|
|
295
316
|
text-overflow: ellipsis;
|
|
296
317
|
white-space: nowrap;
|
|
@@ -298,5 +319,5 @@
|
|
|
298
319
|
|
|
299
320
|
.cardTime {
|
|
300
321
|
font-size: 11px;
|
|
301
|
-
color: var(--
|
|
322
|
+
color: var(--agsc-fg-secondary);
|
|
302
323
|
}
|
|
@@ -58,10 +58,20 @@ export interface SessionCardVM {
|
|
|
58
58
|
gap: boolean
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** Statuses the top-bar count badges can filter down to (UX-01). */
|
|
62
|
+
export type BoardStatusFilter = 'working' | 'waiting'
|
|
63
|
+
|
|
61
64
|
/** Board filter controls (wired to ui.time-window-hours / ui.show-dead). */
|
|
62
65
|
export interface BoardFilterState {
|
|
63
66
|
timeWindowHours: number
|
|
64
67
|
showDead: boolean
|
|
68
|
+
/**
|
|
69
|
+
* Status-only view toggled by the top-bar count badges (UX-01). While
|
|
70
|
+
* set, ONLY sessions of this status are visible — the time window and
|
|
71
|
+
* showDead do not apply (the user explicitly asked for the全板 answer
|
|
72
|
+
* to "who is working/waiting"). Absent = no status filter.
|
|
73
|
+
*/
|
|
74
|
+
statusFilter?: BoardStatusFilter
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
/** Derived status badge: color token + label + attention marker. */
|
|
@@ -69,8 +79,11 @@ export interface StatusBadgeVM {
|
|
|
69
79
|
status: SessionStatusToken
|
|
70
80
|
tone: BadgeTone
|
|
71
81
|
label: string
|
|
72
|
-
/**
|
|
73
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Per-session data-hole marker. The global stream-health notice is NOT
|
|
84
|
+
* mirrored here (UX-18): the top banner already carries it once.
|
|
85
|
+
*/
|
|
86
|
+
attention: 'gap' | null
|
|
74
87
|
attentionLabel: string | null
|
|
75
88
|
}
|
|
76
89
|
|
|
@@ -138,7 +151,9 @@ export interface BoardViewModel {
|
|
|
138
151
|
streamTone: BadgeTone
|
|
139
152
|
visibleCount: number
|
|
140
153
|
totalCount: number
|
|
154
|
+
/** Whole-board counts (window/filter independent — the honest answer). */
|
|
141
155
|
workingCount: number
|
|
156
|
+
waitingCount: number
|
|
142
157
|
}
|
|
143
158
|
|
|
144
159
|
// ---------------------------------------------------------------------------
|
|
@@ -204,6 +219,9 @@ const STATUS_TONE: Record<SessionStatusToken, BadgeTone> = {
|
|
|
204
219
|
|
|
205
220
|
/**
|
|
206
221
|
* Visibility rules (task spec):
|
|
222
|
+
* - an active `statusFilter` (UX-01) overrides everything: only sessions
|
|
223
|
+
* of that status are visible, regardless of window or showDead — the
|
|
224
|
+
* count badge and the filtered board therefore always agree;
|
|
207
225
|
* - dead sessions are hidden unless `showDead`;
|
|
208
226
|
* - working sessions are always visible (even outside the window);
|
|
209
227
|
* - everything else hides once `updatedAtMs` falls strictly beyond the
|
|
@@ -216,6 +234,7 @@ export function isSessionVisible(
|
|
|
216
234
|
nowMs: number,
|
|
217
235
|
): boolean {
|
|
218
236
|
const status = normalizeStatus(session.status)
|
|
237
|
+
if (filters.statusFilter !== undefined) return status === filters.statusFilter
|
|
219
238
|
if (status === 'dead' && !filters.showDead) return false
|
|
220
239
|
if (status === 'working') return true
|
|
221
240
|
const windowMs = filters.timeWindowHours * HOUR_MS
|
|
@@ -295,18 +314,15 @@ export function groupSessions<T extends SessionCardVM>(
|
|
|
295
314
|
// ---------------------------------------------------------------------------
|
|
296
315
|
|
|
297
316
|
/**
|
|
298
|
-
* status + gap
|
|
317
|
+
* status + gap → badge tone/label/attention.
|
|
299
318
|
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
319
|
+
* Card-level attention carries ONLY the per-session `gap` marker (a known
|
|
320
|
+
* data hole for THIS session). The global stream-health state is a
|
|
321
|
+
* board-wide fact and lives in the top banner alone — repeating it on
|
|
322
|
+
* every card was noise, not signal (UX-18). Unknown raw statuses keep
|
|
303
323
|
* their raw text as the label — the board never invents a state.
|
|
304
324
|
*/
|
|
305
|
-
export function deriveBadge(
|
|
306
|
-
rawStatus: string,
|
|
307
|
-
gap: boolean,
|
|
308
|
-
streamHealth: StreamHealthToken,
|
|
309
|
-
): StatusBadgeVM {
|
|
325
|
+
export function deriveBadge(rawStatus: string, gap: boolean): StatusBadgeVM {
|
|
310
326
|
const status = normalizeStatus(rawStatus)
|
|
311
327
|
const trimmed = rawStatus.trim()
|
|
312
328
|
const label =
|
|
@@ -315,9 +331,7 @@ export function deriveBadge(
|
|
|
315
331
|
? BOARD_STRINGS.status.unknown
|
|
316
332
|
: trimmed
|
|
317
333
|
: BOARD_STRINGS.status[status]
|
|
318
|
-
|
|
319
|
-
if (gap) attention = 'gap'
|
|
320
|
-
else if (streamHealth !== 'ok') attention = 'stale'
|
|
334
|
+
const attention: StatusBadgeVM['attention'] = gap ? 'gap' : null
|
|
321
335
|
return {
|
|
322
336
|
status,
|
|
323
337
|
tone: STATUS_TONE[status],
|
|
@@ -355,8 +369,22 @@ export function badgeHoverTitle(
|
|
|
355
369
|
// ---------------------------------------------------------------------------
|
|
356
370
|
|
|
357
371
|
/**
|
|
358
|
-
*
|
|
359
|
-
*
|
|
372
|
+
* Absolute short timestamp in local time: `MM-DD HH:mm`. Used for ages
|
|
373
|
+
* beyond 24h where relative buckets stop discriminating (UX-13).
|
|
374
|
+
*/
|
|
375
|
+
export function formatAbsoluteShort(thenMs: number): string {
|
|
376
|
+
const date = new Date(thenMs)
|
|
377
|
+
const pad = (n: number): string => String(n).padStart(2, '0')
|
|
378
|
+
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(
|
|
379
|
+
date.getMinutes(),
|
|
380
|
+
)}`
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Card time: <60s (including clock skew into the future) is 刚刚, then
|
|
385
|
+
* whole minutes/hours; from 24h on the absolute short date takes over —
|
|
386
|
+
* a column of "3 天前" carries no information, "08-22 14:03" does
|
|
387
|
+
* (UX-13). Non-finite input renders empty.
|
|
360
388
|
*/
|
|
361
389
|
export function formatRelativeTime(thenMs: number, nowMs: number): string {
|
|
362
390
|
if (!Number.isFinite(thenMs)) return ''
|
|
@@ -368,7 +396,7 @@ export function formatRelativeTime(thenMs: number, nowMs: number): string {
|
|
|
368
396
|
if (delta < DAY_MS) {
|
|
369
397
|
return formatTemplate(BOARD_STRINGS.time.hoursAgo, { n: Math.floor(delta / HOUR_MS) })
|
|
370
398
|
}
|
|
371
|
-
return
|
|
399
|
+
return formatAbsoluteShort(thenMs)
|
|
372
400
|
}
|
|
373
401
|
|
|
374
402
|
/** Label for a time-window option: whole days as 天, otherwise 小时. */
|
|
@@ -490,6 +518,47 @@ export function abbreviateSessionId(id: string, max = 20): string {
|
|
|
490
518
|
return `${id.slice(0, 12)}…${id.slice(-6)}`
|
|
491
519
|
}
|
|
492
520
|
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
// Display truncation (UX-02 board groups / UX-20 project lanes).
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
|
|
525
|
+
/** Cards a board group renders before the 「展开全部」 fold (UX-02). */
|
|
526
|
+
export const GROUP_CARD_LIMIT = 20
|
|
527
|
+
|
|
528
|
+
/** A truncated card list plus how many items the fold is hiding. */
|
|
529
|
+
export interface DisplaySlice<T> {
|
|
530
|
+
shown: T[]
|
|
531
|
+
hiddenCount: number
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Slice a status-sorted card list down to `limit` for display, unless
|
|
536
|
+
* `expanded`. The cut never lands inside the leading working/waiting run:
|
|
537
|
+
* attention-worthy sessions are the reason the board exists, so the
|
|
538
|
+
* effective limit grows to cover all of them (an all-active group renders
|
|
539
|
+
* fully — honest, and rare). Non-positive limits disable truncation.
|
|
540
|
+
*/
|
|
541
|
+
export function sliceCardsForDisplay<T extends { status: string }>(
|
|
542
|
+
cards: readonly T[],
|
|
543
|
+
limit: number,
|
|
544
|
+
expanded: boolean,
|
|
545
|
+
): DisplaySlice<T> {
|
|
546
|
+
if (expanded || limit <= 0 || cards.length <= limit) {
|
|
547
|
+
return { shown: [...cards], hiddenCount: 0 }
|
|
548
|
+
}
|
|
549
|
+
let activeRun = 0
|
|
550
|
+
while (activeRun < cards.length) {
|
|
551
|
+
const status = normalizeStatus(cards[activeRun]!.status)
|
|
552
|
+
if (status !== 'working' && status !== 'waiting') break
|
|
553
|
+
activeRun += 1
|
|
554
|
+
}
|
|
555
|
+
const effectiveLimit = Math.max(limit, activeRun)
|
|
556
|
+
return {
|
|
557
|
+
shown: cards.slice(0, effectiveLimit),
|
|
558
|
+
hiddenCount: cards.length - Math.min(cards.length, effectiveLimit),
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
493
562
|
// ---------------------------------------------------------------------------
|
|
494
563
|
// Footer widget derivation.
|
|
495
564
|
// ---------------------------------------------------------------------------
|
|
@@ -510,15 +579,23 @@ export function deriveWidgetConnection(
|
|
|
510
579
|
return 'degraded'
|
|
511
580
|
}
|
|
512
581
|
|
|
513
|
-
/** Count of sessions
|
|
514
|
-
export function
|
|
582
|
+
/** Count of sessions observed in one normalized status. */
|
|
583
|
+
export function countByStatus(
|
|
584
|
+
sessions: ReadonlyArray<{ status: string }>,
|
|
585
|
+
status: SessionStatusToken,
|
|
586
|
+
): number {
|
|
515
587
|
let count = 0
|
|
516
588
|
for (const session of sessions) {
|
|
517
|
-
if (normalizeStatus(session.status) ===
|
|
589
|
+
if (normalizeStatus(session.status) === status) count += 1
|
|
518
590
|
}
|
|
519
591
|
return count
|
|
520
592
|
}
|
|
521
593
|
|
|
594
|
+
/** Count of sessions currently observed as working. */
|
|
595
|
+
export function countWorking(sessions: ReadonlyArray<{ status: string }>): number {
|
|
596
|
+
return countByStatus(sessions, 'working')
|
|
597
|
+
}
|
|
598
|
+
|
|
522
599
|
/** Widget hover/aria text: connection state, plus the count when nonzero. */
|
|
523
600
|
export function widgetTitle(connection: WidgetConnection, workingCount: number): string {
|
|
524
601
|
const base = `${BOARD_STRINGS.widget.label}: ${BOARD_STRINGS.widget.connection[connection]}`
|
|
@@ -536,7 +613,7 @@ export function buildBoardViewModel(input: BoardComputeInput): BoardViewModel {
|
|
|
536
613
|
const visible = filterSessions(sessions, filters, nowMs)
|
|
537
614
|
const derived: DerivedSessionCardVM[] = visible.map((session) => ({
|
|
538
615
|
...session,
|
|
539
|
-
badge: deriveBadge(session.status, session.gap
|
|
616
|
+
badge: deriveBadge(session.status, session.gap),
|
|
540
617
|
glyph: agentGlyph(session.agent),
|
|
541
618
|
shortId: abbreviateSessionId(session.sessionId),
|
|
542
619
|
relativeTime: formatRelativeTime(session.updatedAtMs, nowMs),
|
|
@@ -552,5 +629,6 @@ export function buildBoardViewModel(input: BoardComputeInput): BoardViewModel {
|
|
|
552
629
|
visibleCount: visible.length,
|
|
553
630
|
totalCount: sessions.length,
|
|
554
631
|
workingCount: countWorking(sessions),
|
|
632
|
+
waitingCount: countByStatus(sessions, 'waiting'),
|
|
555
633
|
}
|
|
556
634
|
}
|
|
@@ -13,13 +13,10 @@
|
|
|
13
13
|
* and the group-level `lastActivityAt`. Only the fields this view renders
|
|
14
14
|
* are mirrored; extra wire fields are structurally ignored.
|
|
15
15
|
*
|
|
16
|
-
* Reuse contract (T2.2
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* locales later); strings emitted by reused board tools (status labels,
|
|
21
|
-
* relative time, the unknown-project label) intentionally stay with the
|
|
22
|
-
* board table so the two views can never drift apart.
|
|
16
|
+
* Reuse contract (T2.2): tone/glyph/relative-time/label tools are imported
|
|
17
|
+
* from `./logic.ts`, so both board views speak the same visual language.
|
|
18
|
+
* Project-only copy is exposed through the dynamic
|
|
19
|
+
* {@link PROJECT_VIEW_STRINGS} locale facade.
|
|
23
20
|
*
|
|
24
21
|
* @module
|
|
25
22
|
*/
|
|
@@ -35,39 +32,35 @@ import {
|
|
|
35
32
|
statusRank,
|
|
36
33
|
type StatusBadgeVM,
|
|
37
34
|
} from './logic.ts'
|
|
35
|
+
import { createLocaleView } from '../locales/view.ts'
|
|
38
36
|
|
|
39
37
|
// ---------------------------------------------------------------------------
|
|
40
|
-
//
|
|
38
|
+
// Project-view-only locale facade.
|
|
41
39
|
// ---------------------------------------------------------------------------
|
|
42
40
|
|
|
43
|
-
export const PROJECT_VIEW_STRINGS = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
lastActive: '最近活跃 {time}',
|
|
54
|
-
/** Marker on sessions live in this dsh process (UnifiedSession.live). */
|
|
55
|
-
liveChip: '实时',
|
|
56
|
-
/** Untitled-session fallback. */
|
|
57
|
-
untitled: '(无标题)',
|
|
58
|
-
/** Empty state (no groups at all). */
|
|
41
|
+
export const PROJECT_VIEW_STRINGS = createLocaleView({
|
|
42
|
+
title: 'project.title',
|
|
43
|
+
summary: 'project.summary',
|
|
44
|
+
crossAgent: 'project.crossAgent',
|
|
45
|
+
sessionCount: 'project.sessionCount',
|
|
46
|
+
lastActive: 'project.lastActive',
|
|
47
|
+
liveChip: 'project.liveChip',
|
|
48
|
+
untitled: 'project.untitled',
|
|
49
|
+
showAllSessions: 'project.showAllSessions',
|
|
50
|
+
showLessSessions: 'project.showLessSessions',
|
|
59
51
|
empty: {
|
|
60
|
-
title: '
|
|
61
|
-
hint: '
|
|
52
|
+
title: 'project.empty.title',
|
|
53
|
+
hint: 'project.empty.hint',
|
|
62
54
|
},
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
errorTitle: '项目关联加载失败',
|
|
67
|
-
} as const
|
|
55
|
+
loading: 'project.loading',
|
|
56
|
+
errorTitle: 'project.errorTitle',
|
|
57
|
+
} as const)
|
|
68
58
|
|
|
69
59
|
export type ProjectViewStrings = typeof PROJECT_VIEW_STRINGS
|
|
70
60
|
|
|
61
|
+
/** Rows a lane renders before the 「展开全部」 fold (UX-20). */
|
|
62
|
+
export const LANE_SESSION_LIMIT = 10
|
|
63
|
+
|
|
71
64
|
// ---------------------------------------------------------------------------
|
|
72
65
|
// Input view models (hand-written wire mirror, see module doc).
|
|
73
66
|
// ---------------------------------------------------------------------------
|
|
@@ -224,9 +217,9 @@ function deriveSession(session: ProjectSessionVM, nowMs: number): DerivedProject
|
|
|
224
217
|
lastActivityAt: session.lastActivityAt,
|
|
225
218
|
live,
|
|
226
219
|
gap,
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
badge: deriveBadge(session.status, gap
|
|
220
|
+
// Badge attention reflects only the per-session gap marker (the badge
|
|
221
|
+
// vocabulary has no global-stream mirror at card level, UX-18).
|
|
222
|
+
badge: deriveBadge(session.status, gap),
|
|
230
223
|
glyph: agentGlyph(session.agent),
|
|
231
224
|
shortId: abbreviateSessionId(session.sessionId),
|
|
232
225
|
relativeTime: formatRelativeTime(session.lastActivityAt, nowMs),
|