@kingsoftcloud/agentkit-harness-ui 0.0.0-stage → 0.0.1

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.
Files changed (56) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +116 -0
  3. package/README.md +292 -2
  4. package/markdown/build/build-markdown.cjs +66 -0
  5. package/markdown/dist/THIRD_PARTY_LICENSES.txt +2993 -0
  6. package/markdown/dist/markdown.css +1 -0
  7. package/markdown/dist/markdown.mjs +325 -0
  8. package/markdown/source/markdown-factory.mjs +613 -0
  9. package/markdown/source/markdown.css +288 -0
  10. package/markdown/source/table-factory.mjs +1588 -0
  11. package/markdown/source/table.css +487 -0
  12. package/package.json +96 -4
  13. package/src/activity-groups.mjs +74 -0
  14. package/src/browser-response-groups.mjs +57 -0
  15. package/src/command-output.mjs +57 -0
  16. package/src/components.mjs +9 -0
  17. package/src/context-markers.mjs +19 -0
  18. package/src/context-status.mjs +34 -0
  19. package/src/diff.mjs +240 -0
  20. package/src/explore-tool-call.mjs +139 -0
  21. package/src/file-preview.mjs +50 -0
  22. package/src/host/native-a2a.mjs +90 -0
  23. package/src/host/native-child-file-pane.mjs +49 -0
  24. package/src/host/native-child-pane.mjs +94 -0
  25. package/src/host/native-command-pane.mjs +64 -0
  26. package/src/host/native-run-panel.mjs +202 -0
  27. package/src/host/session-task-events.mjs +247 -0
  28. package/src/host/state/native-a2a.mjs +122 -0
  29. package/src/host/state/native-child-observation.mjs +35 -0
  30. package/src/host/state/native-children.mjs +93 -0
  31. package/src/host/state/native-outcome.mjs +35 -0
  32. package/src/host/state/native-recovery.mjs +187 -0
  33. package/src/host/state/native-runtime-route.mjs +26 -0
  34. package/src/host/state/native-session-stop.mjs +40 -0
  35. package/src/host/state/native-status-panel.mjs +23 -0
  36. package/src/host/state/native-task-read-model.mjs +89 -0
  37. package/src/host/state/task-links.mjs +40 -0
  38. package/src/host/task-timeline.mjs +185 -0
  39. package/src/index.mjs +52 -0
  40. package/src/input-preview.mjs +44 -0
  41. package/src/inspector.mjs +40 -0
  42. package/src/project.mjs +132 -0
  43. package/src/protocol.mjs +362 -0
  44. package/src/react-binding.mjs +486 -0
  45. package/src/reducer.mjs +355 -0
  46. package/src/renderers.mjs +644 -0
  47. package/src/status-panel.mjs +1156 -0
  48. package/src/styles.mjs +278 -0
  49. package/src/summary-playback.mjs +39 -0
  50. package/src/summary.mjs +160 -0
  51. package/src/terminal.mjs +157 -0
  52. package/src/tool-detail.mjs +208 -0
  53. package/src/tool-error.mjs +33 -0
  54. package/src/tool-identity.mjs +40 -0
  55. package/src/view.mjs +364 -0
  56. package/src/work-segments.mjs +41 -0
package/src/styles.mjs ADDED
@@ -0,0 +1,278 @@
1
+ // @kingsoftcloud/agentkit-harness-ui — styles
2
+ //
3
+ // A single CSS string exported for hosts that want the built-in look. It uses
4
+ // only the stable class names produced by the view model and adapts to light
5
+ // and dark color schemes via `prefers-color-scheme`. Hosts can omit it
6
+ // (styleSheet={null}) and own styling entirely, or pass their own string.
7
+
8
+ export const styles = `
9
+ .harness-timeline-shell {
10
+ --harness-fg: currentColor;
11
+ --harness-fg-muted: color-mix(in srgb, currentColor 60%, transparent);
12
+ --harness-bg: transparent;
13
+ --harness-border: color-mix(in srgb, currentColor 14%, transparent);
14
+ --harness-running: #0969da;
15
+ --harness-error: #cf222e;
16
+ --harness-added: #1a7f37;
17
+ --harness-removed: #cf222e;
18
+ --harness-thought: color-mix(in srgb, currentColor 40%, transparent);
19
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
20
+ font-size: var(--ui-font-size, 14px);
21
+ color: var(--harness-fg);
22
+ background: transparent;
23
+ border: 0;
24
+ overflow: visible;
25
+ box-sizing: border-box;
26
+ padding-block: 0 20px;
27
+ container: harness-process / inline-size;
28
+ }
29
+ .harness-timeline-shell[data-has-user-input="true"] { padding-block-start: 56px; }
30
+ @media (prefers-color-scheme: dark) {
31
+ .harness-timeline-shell {
32
+ --harness-running: #4493f8;
33
+ --harness-error: #ff7b72;
34
+ --harness-added: #4ac26b;
35
+ --harness-removed: #ff7b72;
36
+ --harness-thought: color-mix(in srgb, currentColor 30%, transparent);
37
+ }
38
+ }
39
+ .harness-timeline-shell[data-theme="dark"] { --harness-running: #4493f8; --harness-error: #ff7b72; --harness-added: #4ac26b; --harness-removed: #ff7b72; --harness-thought: color-mix(in srgb, currentColor 30%, transparent); }
40
+ .harness-timeline-shell[data-theme="light"] { --harness-running: #0969da; --harness-error: #cf222e; --harness-added: #1a7f37; --harness-removed: #cf222e; --harness-thought: color-mix(in srgb, currentColor 40%, transparent); }
41
+ .harness-timeline {
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: var(--harness-section-gap, 20px);
45
+ padding: 0;
46
+ }
47
+ .harness-plan-dock {
48
+ display: flex;
49
+ flex-wrap: wrap;
50
+ align-items: baseline;
51
+ gap: 8px;
52
+ padding: 8px 10px;
53
+ border-bottom: 1px solid var(--harness-border);
54
+ font-size: 0.85rem;
55
+ }
56
+ .harness-plan-count { font-weight: 600; }
57
+ .harness-plan-label { color: var(--harness-fg-muted); }
58
+ .harness-plan-current { color: var(--harness-running); }
59
+ .harness-thinking-placeholder { color: var(--harness-fg-muted); padding: 4px 2px; }
60
+ .harness-shimmer {
61
+ background: linear-gradient(90deg, var(--harness-fg-muted), var(--harness-fg), var(--harness-fg-muted));
62
+ background-size: 200% 100%;
63
+ -webkit-background-clip: text;
64
+ background-clip: text;
65
+ color: transparent;
66
+ animation: harness-shimmer 1.4s linear infinite;
67
+ }
68
+ @media (prefers-reduced-motion: reduce) {
69
+ .harness-shimmer { animation: none; color: var(--harness-fg-muted); }
70
+ }
71
+ @keyframes harness-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
72
+ .harness-text { white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
73
+ .harness-thought, .harness-tool {
74
+ border: 0;
75
+ padding: 0;
76
+ background: transparent;
77
+ }
78
+ .harness-thought-details, .harness-tool-details { margin: 0; }
79
+ .harness-thought-summary, .harness-tool-summary {
80
+ display: flex;
81
+ flex-wrap: nowrap;
82
+ align-items: center;
83
+ gap: 8px;
84
+ cursor: pointer;
85
+ list-style: none;
86
+ user-select: none;
87
+ }
88
+ .harness-thought-summary::-webkit-details-marker, .harness-tool-summary::-webkit-details-marker { display: none; }
89
+ /* Inline SVG icons (Lucide, ISC — see NOTICE) are sized by font-size so they
90
+ sit on the text baseline; stroke is currentColor so they inherit the line
91
+ color and dim with the muted/failure tokens. */
92
+ .harness-icon { width: 16px; height: 16px; display: inline-block; flex: 0 0 auto; vertical-align: -0.15em; }
93
+ .harness-tool-kind-icon, .harness-file-icon { color: var(--harness-fg-muted); }
94
+ .harness-thought-icon { color: var(--harness-thought); }
95
+ .harness-tool-chevron, .harness-thought-chevron {
96
+ color: var(--harness-fg-muted); flex: 0 0 auto; transition: transform 0.2s ease-out, opacity 0.2s ease-out;
97
+ opacity: 0;
98
+ }
99
+ .harness-tool-summary:hover .harness-tool-chevron,
100
+ .harness-thought-summary:hover .harness-thought-chevron { opacity: 1; }
101
+ .harness-tool-details[open] .harness-tool-chevron,
102
+ .harness-thought-details[open] .harness-thought-chevron { transform: rotate(90deg); opacity: 1; }
103
+ @media (prefers-reduced-motion: reduce) {
104
+ .harness-tool-chevron, .harness-thought-chevron { transition: none; }
105
+ }
106
+ .harness-thought-label { color: var(--harness-thought); font-weight: 500; flex-shrink: 0; white-space: nowrap; }
107
+ .harness-thought-preview { color: var(--harness-fg-muted); font-style: normal; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60ch; }
108
+ .harness-thought-body { margin: 12px 0 0 8px; white-space: pre-wrap; word-break: break-word; max-height: 240px; overflow: auto; padding-left: 14px; border-left: 1px solid var(--harness-border); color: var(--harness-thought); }
109
+ .harness-thought-label-streaming { color: var(--harness-running); }
110
+ .harness-thought-preview {
111
+ color: var(--harness-fg-muted); font-style: normal; overflow: hidden; white-space: nowrap;
112
+ text-overflow: ellipsis; min-width: 0; flex: 1 1 auto; max-width: 60ch;
113
+ }
114
+ .harness-thought-preview-text { display: inline-block; min-width: 100%; }
115
+ .harness-tool-title { font-weight: 500; color: var(--harness-fg-muted); flex-shrink: 0; white-space: nowrap; }
116
+ /* Running tool kind/title takes the streaming gradient text (matches the
117
+ desktop ToolLayout running kindLabel); reduced-motion falls back to the
118
+ running color without the sweep. */
119
+ .harness-tool-title-streaming {
120
+ background: linear-gradient(90deg, var(--harness-fg-muted), var(--harness-running), var(--harness-fg-muted));
121
+ background-size: 200% 100%;
122
+ -webkit-background-clip: text; background-clip: text; color: transparent;
123
+ animation: harness-shimmer 1.4s linear infinite;
124
+ }
125
+ @media (prefers-reduced-motion: reduce) {
126
+ .harness-tool-title-streaming { animation: none; color: var(--harness-running); }
127
+ }
128
+ .harness-search-primary, .harness-tool-primary { color: var(--harness-fg); font-weight: 400; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
129
+ .harness-tool-command-summary {
130
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.8rem;
131
+ color: var(--harness-fg-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
132
+ max-width: 50ch;
133
+ }
134
+ .harness-tool-secondary { font-size: 0.8rem; color: var(--harness-fg-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 40ch; }
135
+ .harness-tool-status { font-size: 0.8rem; color: var(--harness-fg-muted); text-transform: lowercase; }
136
+ .harness-tool-status-running { color: var(--harness-running); }
137
+ .harness-tool-status-failed { color: var(--harness-error); }
138
+ .harness-tool-status-completed { display: none; }
139
+ .harness-tool-details-static > summary { cursor: default; list-style: none; }
140
+ .harness-tool-details-static > summary::-webkit-details-marker { display: none; }
141
+ .harness-tool-action {
142
+ border: 1px solid var(--harness-border); background: transparent; color: var(--harness-fg-muted);
143
+ border-radius: 6px; padding: 2px 8px; font-size: 0.75rem; cursor: pointer;
144
+ }
145
+ .harness-tool-action:hover { border-color: var(--harness-running); color: var(--harness-running); }
146
+ .harness-inspect-action { padding: 1px 6px; }
147
+ .harness-copy-action { font-family: ui-sans-serif, system-ui, sans-serif; }
148
+ .harness-tool-detail { margin: 6px 0 0; display: flex; flex-direction: column; gap: 2px; font-size: 0.85rem; }
149
+ .harness-tool-detail-line { display: flex; gap: 6px; }
150
+ .harness-tool-detail-line dt { color: var(--harness-fg-muted); min-width: 8ch; }
151
+ .harness-tool-detail-line dd { margin: 0; word-break: break-all; }
152
+ .harness-tool-progress { color: var(--harness-running); font-size: 0.85rem; }
153
+ .harness-tool-error { color: var(--harness-error); font-size: 0.85rem; }
154
+ .harness-tool-error-inline { font-weight: 600; }
155
+ .harness-tool-result { margin-top: 6px; }
156
+ .harness-tool-result > summary { cursor: pointer; color: var(--harness-fg-muted); font-size: 0.8rem; }
157
+
158
+ /* ── execute / terminal ─────────────────────────────────────────────── */
159
+ .harness-execute-detail { display: flex; flex-direction: column; gap: 8px; margin-top: 6px; }
160
+ .harness-execute-command { min-width: 0; }
161
+ .harness-execute-command-row { display: flex; align-items: flex-start; gap: 6px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
162
+ .harness-execute-prompt { color: var(--harness-fg-muted); flex: 0 0 auto; }
163
+ .harness-execute-command-text {
164
+ margin: 0; min-width: 0; flex: 1 1 auto; white-space: pre-wrap; overflow-wrap: anywhere;
165
+ font-size: inherit; line-height: 20px;
166
+ }
167
+ .harness-command-viewport { max-height: 3lh; overflow: hidden; line-height: 20px; }
168
+ .harness-command-viewport[data-expanded="true"] { max-height: 10lh; overflow-y: auto; }
169
+ .harness-command-toggle { border: 0; padding: 4px 0 0; margin-left: 2ch; background: transparent; color: var(--harness-fg-muted); font: inherit; font-size: 12px; cursor: pointer; }
170
+ .harness-execute-command-actions { display: flex; gap: 6px; margin-top: 4px; flex-wrap: wrap; }
171
+ .harness-execute-output-block { display: flex; flex-direction: column; gap: 4px; }
172
+ .harness-execute-output {
173
+ margin: 0; min-width: 0; max-height: 5lh; overflow: auto; padding: 0;
174
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: inherit;
175
+ white-space: pre-wrap; overflow-wrap: anywhere; line-height: 20px; color: var(--harness-fg-muted);
176
+ }
177
+ .harness-execute-stdout { background: transparent; }
178
+ .harness-execute-stderr { color: var(--harness-error); background: color-mix(in srgb, var(--harness-error) 8%, transparent); }
179
+ .harness-execute-no-output { color: var(--harness-fg-muted); font-size: 0.8rem; margin: 0; }
180
+ .harness-execute-meta { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 0.75rem; }
181
+ .harness-execute-exit { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; padding: 1px 6px; border-radius: 4px; }
182
+ .harness-execute-exit-ok { color: var(--harness-added); background: color-mix(in srgb, var(--harness-added) 12%, transparent); }
183
+ .harness-execute-exit-err { color: var(--harness-error); background: color-mix(in srgb, var(--harness-error) 12%, transparent); }
184
+ .harness-execute-cwd { color: var(--harness-fg-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
185
+
186
+ /* ── file chips (read / edit) ───────────────────────────────────────── */
187
+ .harness-edit-chips { display: inline-flex; flex-wrap: wrap; gap: 2px; align-items: center; min-width: 0; }
188
+ .harness-edit-chip-wrap { display: inline-flex; min-width: 0; align-items: center; }
189
+ .harness-edit-chip-sep { color: var(--harness-fg-muted); margin: 0 2px; }
190
+ .harness-file-chip {
191
+ display: inline-flex; min-width: 0; max-width: 100%; align-items: center; gap: 4px;
192
+ color: var(--harness-fg-muted); background: transparent; border: 0; cursor: pointer; padding: 0;
193
+ }
194
+ .harness-file-chip:hover { color: var(--harness-running); text-decoration: underline; }
195
+ .harness-file-chip-static { cursor: default; }
196
+ .harness-file-chip-static:hover { color: var(--harness-fg-muted); text-decoration: none; }
197
+ .harness-file-icon { font-size: 0.85rem; }
198
+ .harness-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
199
+ .harness-read-path { font-size: 0.78rem; }
200
+
201
+ /* ── diff count + inline diff (edit) ─────────────────────────────────── */
202
+ .harness-diff-count { display: inline-flex; gap: 4px; align-items: center; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.78rem; }
203
+ .harness-diff-added { color: var(--harness-added); }
204
+ .harness-diff-removed { color: var(--harness-removed); }
205
+ .harness-edit-diff { margin-top: 6px; border-top: 1px solid var(--harness-border); padding-top: 6px; }
206
+ .harness-diff-header { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
207
+ .harness-diff-path { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.8rem; color: var(--harness-fg); word-break: break-all; }
208
+ .harness-diff-actions { display: inline-flex; gap: 6px; margin-left: auto; }
209
+ .harness-diff-view { margin-top: 6px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.78rem; }
210
+ .harness-diff-line { display: grid; grid-template-columns: 3ch 3ch 1.5ch 1fr; white-space: pre; }
211
+ .harness-diff-gutter { color: var(--harness-fg-muted); text-align: right; padding-right: 4px; user-select: none; opacity: 0.7; }
212
+ .harness-diff-marker { text-align: center; user-select: none; opacity: 0.7; }
213
+ .harness-diff-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
214
+ .harness-diff-line:hover .harness-diff-text { overflow: visible; text-overflow: clip; }
215
+ .harness-diff-added > .harness-diff-text, .harness-diff-added { color: var(--harness-added); background: color-mix(in srgb, var(--harness-added) 10%, transparent); }
216
+ .harness-diff-removed > .harness-diff-text, .harness-diff-removed { color: var(--harness-removed); background: color-mix(in srgb, var(--harness-removed) 10%, transparent); }
217
+ .harness-diff-truncated { color: var(--harness-fg-muted); padding: 2px 0; }
218
+
219
+ /* Split view: default hidden on narrow viewports, shown alongside unified on wide. */
220
+ .harness-diff-split { display: none; }
221
+ .harness-diff-split-col { min-width: 0; overflow: auto; }
222
+ @container harness-process (min-width: 720px) {
223
+ .harness-timeline-shell.wide-diff .harness-diff-unified,
224
+ .harness-timeline-shell:has(.harness-diff-split) .harness-diff-unified { display: none; }
225
+ .harness-diff-split { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; }
226
+ .harness-diff-split-col { border: 1px solid var(--harness-border); }
227
+ }
228
+
229
+ .harness-jump-latest {
230
+ align-self: center; border: 1px solid var(--harness-border); background: var(--harness-bg);
231
+ color: var(--harness-fg); border-radius: 999px; padding: 4px 12px; cursor: pointer;
232
+ }
233
+ .harness-jump-latest:hover { border-color: var(--harness-running); color: var(--harness-running); }
234
+
235
+ .harness-user-message { margin: 0 0 0 auto; padding: .8rem 1rem; max-width: 85%; border-radius: 14px; background: color-mix(in srgb, currentColor 7%, transparent); white-space: pre-wrap; overflow-wrap: anywhere; }
236
+
237
+ .harness-summary-roll { position: relative; display: inline-grid; vertical-align: middle; overflow: hidden; }
238
+ .harness-summary-roll > span { grid-area: 1 / 1; white-space: nowrap; }
239
+ .harness-summary-in { animation: harness-summary-in 300ms cubic-bezier(.4,0,.2,1) both; }
240
+ .harness-summary-out { position: absolute; left: 0; top: 0; animation: harness-summary-out 300ms cubic-bezier(.4,0,.2,1) both; }
241
+ @keyframes harness-summary-in { from { transform: translateY(.8em); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
242
+ @keyframes harness-summary-out { from { transform: translateY(0); opacity: 1; } to { transform: translateY(-.8em); opacity: 0; } }
243
+ @media (prefers-reduced-motion: reduce) { .harness-summary-in, .harness-summary-out { position: absolute; left: 0; top: 0; animation: none; } }
244
+
245
+ .harness-context-marker { display: flex; align-items: center; gap: 10px; padding: 8px 0; color: var(--harness-fg-muted); font-size: var(--ui-font-size, 14px); }
246
+ .harness-context-rule { height: 1px; min-width: 24px; flex: 1; background: var(--harness-border); opacity: .5; }
247
+ .harness-context-marker > svg { flex-shrink: 0; }
248
+
249
+ button.harness-tool-summary { border: 0; background: none; color: inherit; font: inherit; text-align: left; padding: 0; cursor: pointer; max-width: 100%; }
250
+ .harness-agent-name { display: inline-flex; align-items: center; flex-shrink: 0; max-width: 9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 500; line-height: 1.5; }
251
+ .harness-agent-title { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--harness-fg-muted); }
252
+ .harness-agent-separator { flex: 0 0 auto; color: var(--harness-fg-muted); }
253
+ .harness-agent-name[data-color="blue"] { color: #0369a1; } .harness-agent-name[data-color="cyan"] { color: #0e7490; }
254
+ .harness-agent-name[data-color="green"] { color: #047857; } .harness-agent-name[data-color="orange"] { color: #c2410c; }
255
+ .harness-agent-name[data-color="pink"] { color: #be185d; } .harness-agent-name[data-color="purple"] { color: #6d28d9; }
256
+ .harness-agent-name[data-color="red"] { color: #be123c; } .harness-agent-name[data-color="yellow"] { color: #b45309; }
257
+ [data-theme="dark"] .harness-agent-name[data-color="blue"] { color: #7dd3fc; } [data-theme="dark"] .harness-agent-name[data-color="cyan"] { color: #67e8f9; }
258
+ [data-theme="dark"] .harness-agent-name[data-color="green"] { color: #6ee7b7; } [data-theme="dark"] .harness-agent-name[data-color="orange"] { color: #fdba74; }
259
+ [data-theme="dark"] .harness-agent-name[data-color="pink"] { color: #f9a8d4; } [data-theme="dark"] .harness-agent-name[data-color="purple"] { color: #c4b5fd; }
260
+ [data-theme="dark"] .harness-agent-name[data-color="red"] { color: #fda4af; } [data-theme="dark"] .harness-agent-name[data-color="yellow"] { color: #fcd34d; }
261
+
262
+ .harness-subagent-pane { padding: 16px; height: 100%; min-height: 0; overflow: auto; }
263
+ .harness-subagent-window-notice { font-size: 12px; color: var(--harness-fg-muted, #888); }
264
+ .harness-work-items { display: flex; flex-direction: column; gap: var(--harness-work-gap, 16px); }
265
+ .harness-work-history { margin: 0; }
266
+ .harness-work-history-summary { display: flex; align-items: center; gap: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--harness-border); color: var(--harness-fg-muted); cursor: pointer; list-style: none; }
267
+ .harness-work-history-summary::-webkit-details-marker { display: none; }
268
+ .harness-work-history-chevron { opacity: .7; }
269
+ .harness-work-history[open] > summary .harness-work-history-chevron { transform: rotate(90deg); }
270
+ .harness-work-history-content { padding-top: var(--harness-section-gap, 20px); }
271
+ .harness-activity-summary { display: flex; align-items: center; gap: 8px; min-width: 0; cursor: pointer; color: var(--harness-fg-muted); list-style: none; }
272
+ .harness-activity-summary::-webkit-details-marker { display: none; }
273
+ .harness-activity-summary > svg { flex-shrink: 0; }
274
+ .harness-activity-summary .harness-tool-chevron { opacity: 0; }
275
+ .harness-activity-summary:hover .harness-tool-chevron, .harness-activity-summary:focus-visible .harness-tool-chevron { opacity: 1; }
276
+ .harness-activity-details[open] > summary .harness-tool-chevron { transform: rotate(90deg); opacity: 1; }
277
+ .harness-activity-children { border-left: 1px solid var(--harness-border); margin: 8px 0 8px 7px; padding-left: 16px; display: flex; flex-direction: column; gap: 8px; }
278
+ `;
@@ -0,0 +1,39 @@
1
+ // Presentation scheduling for provider-supplied lines. Never generates text.
2
+ export const SUMMARY_ROLL_TRANSITION_MS = 300;
3
+ export const SUMMARY_ROLL_TOTAL_MS = 800;
4
+ export function latestSummaryLine(text) {
5
+ const lines = String(text ?? '').replace(/\r\n?/g, '\n').split('\n');
6
+ for (let i = lines.length - 1; i >= 0; i--) if (lines[i].trim()) return { key: String(i), text: lines[i].trim() };
7
+ return { key: '0', text: '' };
8
+ }
9
+ export function createSummaryPlayback(initial, publish, clock = {
10
+ now: () => performance.now(), set: (fn, ms) => setTimeout(fn, ms), clear: id => clearTimeout(id),
11
+ }) {
12
+ let current = initial, queue = [], timer = null, disposed = false;
13
+ const clear = () => { if (timer !== null) clock.clear(timer); timer = null; };
14
+ const promote = next => {
15
+ const previous = current; current = next; publish({ current, previous, animate: true });
16
+ const expected = clock.now() + SUMMARY_ROLL_TOTAL_MS;
17
+ timer = clock.set(() => {
18
+ timer = null;
19
+ if (disposed) return;
20
+ if (clock.now() - expected > 250 && queue.length > 1) queue = queue.slice(-1);
21
+ const next = queue.shift(); if (next) promote(next);
22
+ }, SUMMARY_ROLL_TOTAL_MS);
23
+ };
24
+ return {
25
+ update(next, animate = true) {
26
+ if (disposed) return;
27
+ if (!animate) { clear(); queue = []; current = next; publish({ current, previous: null, animate: false }); return; }
28
+ if (current.key === next.key) {
29
+ if (current.text !== next.text) { current = next; publish({ current, animate: false, refresh: true }); }
30
+ return;
31
+ }
32
+ if (timer === null && queue.length === 0) { promote(next); return; }
33
+ const index = queue.findIndex(item => item.key === next.key);
34
+ if (index >= 0) queue[index] = next;
35
+ else queue = queue.length === 0 ? [next] : [queue[0], next];
36
+ },
37
+ dispose() { disposed = true; clear(); queue = []; },
38
+ };
39
+ }
@@ -0,0 +1,160 @@
1
+ // @kingsoftcloud/agentkit-harness-ui — bounded text helpers
2
+ //
3
+ // Pure, deterministic text utilities. No timers, no DOM. The animation timing
4
+ // from the reference LiveThinking/LatestSummaryLine stays in the React layer;
5
+ // this module owns the line-selection and truncation policy that the layer
6
+ // consumes, so the meaningful behavior is testable without a clock.
7
+
8
+ /** Keys whose values are redacted from any bounded preview. */
9
+ export const SENSITIVE_KEYS = Object.freeze(new Set([
10
+ "token", "tokens", "secret", "password", "passwd", "api_key", "apikey",
11
+ "authorization", "auth", "cookie", "session", "private_key", "key",
12
+ ]));
13
+
14
+ /**
15
+ * Latest non-empty, trimmed line of a streaming text. Used as the collapsed
16
+ * preview of a thought while it streams, so the user sees progress without the
17
+ * full reasoning being forced into view. Returns "" for blank text.
18
+ * @param {string} text
19
+ * @returns {string}
20
+ */
21
+ export function latestNonEmptyLine(text) {
22
+ const value = text == null ? "" : String(text);
23
+ const lines = value.split("\n");
24
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
25
+ const line = lines[i].trim();
26
+ if (line) return line;
27
+ }
28
+ return "";
29
+ }
30
+
31
+ /**
32
+ * Bounded truncation. Keeps at most `maxLines` lines and at most `maxChars`
33
+ * characters per line, appending a single `…` truncation marker when anything
34
+ * is dropped. Never dumps large content verbatim.
35
+ * @param {string} text
36
+ * @param {{maxLines?: number, maxChars?: number}} [opts]
37
+ * @returns {string}
38
+ */
39
+ export function truncateText(text, opts = {}) {
40
+ const maxLines = opts.maxLines && opts.maxLines > 0 ? Math.floor(opts.maxLines) : 0;
41
+ const maxChars = opts.maxChars && opts.maxChars > 0 ? Math.floor(opts.maxChars) : 0;
42
+ const value = text == null ? "" : String(text);
43
+ if (!value) return "";
44
+ const lines = value.split("\n");
45
+ let truncated = false;
46
+ const kept = [];
47
+ for (const line of lines) {
48
+ if (maxLines && kept.length >= maxLines) { truncated = true; break; }
49
+ if (maxChars && line.length > maxChars) {
50
+ kept.push(line.slice(0, Math.max(0, maxChars - 1)) + "…");
51
+ truncated = true;
52
+ } else {
53
+ kept.push(line);
54
+ }
55
+ }
56
+ if (truncated && !kept.some(l => l.endsWith("…"))) kept.push("…");
57
+ return kept.join("\n");
58
+ }
59
+
60
+ /**
61
+ * Redact a value when its key looks sensitive. Returns the original value for
62
+ * safe keys. For `env`/environment maps, keeps the names being set/unset but
63
+ * drops the values, matching the tool-detail-block safety contract.
64
+ * @param {string} key
65
+ * @param {unknown} value
66
+ * @returns {unknown}
67
+ */
68
+ export function redactValue(key, value) {
69
+ const lower = String(key == null ? "" : key).toLowerCase();
70
+ if (SENSITIVE_KEYS.has(lower)) return "[redacted]";
71
+ if (lower === "env" && value && typeof value === "object" && !Array.isArray(value)) {
72
+ const names = Object.keys(/** @type {Record<string, unknown>} */ (value));
73
+ return Object.fromEntries(names.map(name => [name, "[redacted]"]));
74
+ }
75
+ return value;
76
+ }
77
+
78
+ /**
79
+ * Produce a bounded object preview for unknown/MCP tools or fallback display.
80
+ * Redacts sensitive keys, truncates long string values, and keeps at most
81
+ * `maxLines` rows so a single detail block stays small.
82
+ * @param {Record<string, unknown>} input
83
+ * @param {{maxLines?: number, maxChars?: number}} [opts]
84
+ * @returns {{key: string, value: string}[]}
85
+ */
86
+ export function boundedDetailLines(input, opts = {}) {
87
+ const maxLines = opts.maxLines && opts.maxLines > 0 ? Math.floor(opts.maxLines) : 4;
88
+ const maxChars = opts.maxChars && opts.maxChars > 0 ? Math.floor(opts.maxChars) : 80;
89
+ if (!input || typeof input !== "object" || Array.isArray(input)) return [];
90
+ const rows = [];
91
+ for (const [key, raw] of Object.entries(input)) {
92
+ if (rows.length >= maxLines) { rows.push({ key: "…", value: "" }); break; }
93
+ const value = redactValue(key, raw);
94
+ rows.push({ key, value: previewValue(value, maxChars) });
95
+ }
96
+ return rows;
97
+ }
98
+
99
+ function previewValue(value, maxChars) {
100
+ if (value == null) return String(value);
101
+ if (typeof value === "string") return truncateText(value, { maxLines: 1, maxChars });
102
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
103
+ try {
104
+ return truncateText(JSON.stringify(value), { maxLines: 1, maxChars });
105
+ } catch {
106
+ return truncateText(String(value), { maxLines: 1, maxChars });
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Deterministic summary-line queue. Mirrors the reference LatestSummaryLine
112
+ * policy: refresh the current line in place when the newest non-empty line is
113
+ * the same line (grew in place), otherwise queue at most two new lines and
114
+ * promote the oldest queued line to active. This is a pure state machine over
115
+ * the observed text history; the component owns the animation timers.
116
+ */
117
+ export class SummaryQueue {
118
+ constructor() {
119
+ this.active = { key: 0, text: "" };
120
+ this.pending = [];
121
+ }
122
+
123
+ /**
124
+ * @param {string} text the full streamed text so far
125
+ * @returns {{active: {key:number, text:string}, outgoing: {key:number, text:string}|null}}
126
+ */
127
+ push(text) {
128
+ const next = latestNonEmptyLine(text);
129
+ if (next.key === "" && text === "") return { active: this.active, outgoing: null };
130
+ const key = lineKey(text, next);
131
+ if (key === this.active.key) {
132
+ this.active = { key, text: next };
133
+ return { active: this.active, outgoing: null };
134
+ }
135
+ const existing = this.pending.findIndex(line => line.key === key);
136
+ if (existing >= 0) this.pending[existing] = { key, text: next };
137
+ else this.pending = this.pending.length >= 2 ? [this.pending[1], { key, text: next }] : [...this.pending, { key, text: next }];
138
+ const outgoing = this.active;
139
+ const promoted = this.pending.shift();
140
+ if (promoted) this.active = promoted;
141
+ return { active: this.active, outgoing };
142
+ }
143
+
144
+ /** Replace state without producing a transition (reduced-motion / completion). */
145
+ setLatest(text) {
146
+ const next = latestNonEmptyLine(text);
147
+ this.active = { key: lineKey(text, next), text: next };
148
+ this.pending = [];
149
+ return { active: this.active, outgoing: null };
150
+ }
151
+ }
152
+
153
+ function lineKey(fullText, line) {
154
+ // Stable key: the index of the latest non-empty line within the full text.
155
+ const lines = String(fullText).split("\n");
156
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
157
+ if (lines[i].trim() === line) return i;
158
+ }
159
+ return 0;
160
+ }
@@ -0,0 +1,157 @@
1
+ // @kingsoftcloud/agentkit-harness-ui — terminal command/output helpers
2
+ //
3
+ // Pure extraction and sanitization for the terminal (execute) renderer. The
4
+ // command is derived from the host-supplied tool input across the common
5
+ // shapes (string, argv array, object with command/cmd/script/parsed_cmd);
6
+ // the output is derived from the `tool.result` display payload or the result
7
+ // body. ANSI escape sequences are stripped to plain text for the inline
8
+ // bounded preview — the preview NEVER uses innerHTML, so control sequences
9
+ // cannot inject markup. A host that wants a live PTY renders it through the
10
+ // `onOpenTerminal` callback, which is the only path to a real terminal.
11
+ //
12
+ // No invented success: a command with input but no result/exit renders as
13
+ // running, never as completed. Exit code and cwd are shown only when the host
14
+ // supplied them in the display payload.
15
+
16
+ const SHELL_PREFIX_RE = /^(?:\/bin\/|\/usr\/bin\/)?(?:zsh|bash|sh)\s+-lc\s+/i;
17
+
18
+ /**
19
+ * Extract the shell command string from a tool input across common shapes.
20
+ * Strips a leading `zsh/bash/sh -lc ` invocation wrapper. Returns "" when no
21
+ * command can be recovered.
22
+ * @param {unknown} input
23
+ * @returns {string}
24
+ */
25
+ export function extractCommand(input) {
26
+ const fromString = (s) => {
27
+ const trimmed = String(s == null ? "" : s).trim();
28
+ if (!trimmed) return "";
29
+ return trimmed.replace(SHELL_PREFIX_RE, "").trim();
30
+ };
31
+ if (typeof input === "string") return fromString(input);
32
+ if (Array.isArray(input) && input.every(i => typeof i === "string")) {
33
+ const parts = input.map(i => i.trim()).filter(Boolean);
34
+ if (!parts.length) return "";
35
+ const lc = parts.findIndex(p => p === "-lc");
36
+ if (lc >= 0 && parts[lc + 1]) return parts[lc + 1];
37
+ // argv with a leading shell binary: drop the binary, join the rest.
38
+ if (/^(?:\/bin\/|\/usr\/bin\/)?(?:zsh|bash|sh)$/i.test(parts[0])) return parts.slice(1).join(" ");
39
+ return parts.join(" ");
40
+ }
41
+ if (input && typeof input === "object" && !Array.isArray(input)) {
42
+ const rec = /** @type {Record<string, unknown>} */ (input);
43
+ for (const key of ["command", "cmd", "script"]) {
44
+ if (typeof rec[key] === "string") return fromString(rec[key]);
45
+ }
46
+ const parsed = rec.parsed_cmd;
47
+ if (Array.isArray(parsed)) {
48
+ for (const item of parsed) {
49
+ if (typeof item === "string") {
50
+ const c = fromString(item);
51
+ if (c) return c;
52
+ continue;
53
+ }
54
+ if (item && typeof item === "object") {
55
+ const r = /** @type {Record<string, unknown>} */ (item);
56
+ for (const key of ["cmd", "command", "script"]) {
57
+ if (typeof r[key] === "string") {
58
+ const c = fromString(r[key]);
59
+ if (c) return c;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ return "";
67
+ }
68
+
69
+ /**
70
+ * Extract output text from a tool result body or display payload. The display
71
+ * payload (host-supplied `command_output`) is authoritative; the result body
72
+ * is a fallback for hosts that only emit a raw result.
73
+ * @param {unknown} result
74
+ * @param {Record<string, unknown>} [display]
75
+ * @returns {{stdout: string, stderr: string, exit: number|null, cwd: string}}
76
+ */
77
+ export function extractOutput(result, display) {
78
+ const disp = isPlainRecord(display) ? display : {};
79
+ const stdout = pickString(disp, ["stdout", "output", "fullText", "text", "content", "result"])
80
+ ?? extractResultText(result);
81
+ const stderr = pickString(disp, ["stderr", "error", "errorMessage"]) ?? "";
82
+ const exit = parseExit(disp.exit ?? disp.exitCode ?? disp.code ?? disp.returncode);
83
+ const cwd = normalizeCwd(typeof disp.cwd === "string" ? disp.cwd : "");
84
+ return { stdout: sanitizeAnsi(String(stdout || "")), stderr: sanitizeAnsi(String(stderr || "")), exit, cwd };
85
+ }
86
+
87
+ /**
88
+ * Strip ANSI CSI/SGR and other control sequences to plain text. The inline
89
+ * preview renders this as text content (never innerHTML), so escape sequences
90
+ * cannot inject markup. Keeps newlines and tabs; drops bells and other C0
91
+ * controls that would only noisify a bounded preview.
92
+ * @param {string} text
93
+ * @returns {string}
94
+ */
95
+ export function sanitizeAnsi(text) {
96
+ const value = String(text == null ? "" : text);
97
+ if (!value) return "";
98
+ return value
99
+ // CSI sequences: ESC [ ... letter (SGR colors, cursor moves, erases, etc.)
100
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
101
+ // OSC sequences: ESC ] ... BEL or ST (title set, hyperlinks, etc.)
102
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
103
+ // Other two-char ESC sequences (M-, etc.)
104
+ .replace(/\x1b[=>78]/g, "")
105
+ // Stray ESC and BEL, vertical tab / form feed, other C0 controls except
106
+ // tab (\t, 0x09), LF (\n, 0x0a), CR (\r, 0x0d).
107
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
108
+ }
109
+
110
+ /** Normalize a cwd string: trim and drop a single trailing slash. */
111
+ export function normalizeCwd(cwd) {
112
+ const value = String(cwd == null ? "" : cwd).trim();
113
+ if (!value) return "";
114
+ return value.length > 1 && value.endsWith("/") ? value.slice(0, -1) : value;
115
+ }
116
+
117
+ function parseExit(value) {
118
+ if (value == null || value === "") return null;
119
+ const n = Number(value);
120
+ return Number.isFinite(n) ? Math.trunc(n) : null;
121
+ }
122
+
123
+ function isPlainRecord(value) {
124
+ return typeof value === "object" && value !== null && !Array.isArray(value);
125
+ }
126
+
127
+ function pickString(rec, keys) {
128
+ for (const key of keys) {
129
+ const v = rec[key];
130
+ if (typeof v === "string" && v.trim()) return v;
131
+ }
132
+ return null;
133
+ }
134
+
135
+ function extractResultText(output) {
136
+ if (output == null) return "";
137
+ if (typeof output === "string") return output.trim() ? output : "";
138
+ if (typeof output === "number" || typeof output === "boolean" || typeof output === "bigint") return String(output);
139
+ if (Array.isArray(output)) {
140
+ return output.map(extractResultText).filter(t => t).join("\n");
141
+ }
142
+ if (isPlainRecord(output)) {
143
+ const direct = pickString(output, ["output", "text", "content", "result", "stdout", "message"]);
144
+ if (direct) return direct;
145
+ if (isPlainRecord(output.rawOutput)) {
146
+ const nested = extractResultText(output.rawOutput);
147
+ if (nested) return nested;
148
+ }
149
+ if (Array.isArray(output.content)) {
150
+ const joined = output.content
151
+ .map(item => typeof item === "string" ? item.trim() : isPlainRecord(item) ? pickString(item, ["output", "text", "content", "result", "stdout", "message"]) ?? "" : "")
152
+ .filter(Boolean).join("\n");
153
+ if (joined) return joined;
154
+ }
155
+ }
156
+ return "";
157
+ }