@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.
- package/LICENSE +201 -0
- package/NOTICE +116 -0
- package/README.md +292 -2
- package/markdown/build/build-markdown.cjs +66 -0
- package/markdown/dist/THIRD_PARTY_LICENSES.txt +2993 -0
- package/markdown/dist/markdown.css +1 -0
- package/markdown/dist/markdown.mjs +325 -0
- package/markdown/source/markdown-factory.mjs +613 -0
- package/markdown/source/markdown.css +288 -0
- package/markdown/source/table-factory.mjs +1588 -0
- package/markdown/source/table.css +487 -0
- package/package.json +96 -4
- package/src/activity-groups.mjs +74 -0
- package/src/browser-response-groups.mjs +57 -0
- package/src/command-output.mjs +57 -0
- package/src/components.mjs +9 -0
- package/src/context-markers.mjs +19 -0
- package/src/context-status.mjs +34 -0
- package/src/diff.mjs +240 -0
- package/src/explore-tool-call.mjs +139 -0
- package/src/file-preview.mjs +50 -0
- package/src/host/native-a2a.mjs +90 -0
- package/src/host/native-child-file-pane.mjs +49 -0
- package/src/host/native-child-pane.mjs +94 -0
- package/src/host/native-command-pane.mjs +64 -0
- package/src/host/native-run-panel.mjs +202 -0
- package/src/host/session-task-events.mjs +247 -0
- package/src/host/state/native-a2a.mjs +122 -0
- package/src/host/state/native-child-observation.mjs +35 -0
- package/src/host/state/native-children.mjs +93 -0
- package/src/host/state/native-outcome.mjs +35 -0
- package/src/host/state/native-recovery.mjs +187 -0
- package/src/host/state/native-runtime-route.mjs +26 -0
- package/src/host/state/native-session-stop.mjs +40 -0
- package/src/host/state/native-status-panel.mjs +23 -0
- package/src/host/state/native-task-read-model.mjs +89 -0
- package/src/host/state/task-links.mjs +40 -0
- package/src/host/task-timeline.mjs +185 -0
- package/src/index.mjs +52 -0
- package/src/input-preview.mjs +44 -0
- package/src/inspector.mjs +40 -0
- package/src/project.mjs +132 -0
- package/src/protocol.mjs +362 -0
- package/src/react-binding.mjs +486 -0
- package/src/reducer.mjs +355 -0
- package/src/renderers.mjs +644 -0
- package/src/status-panel.mjs +1156 -0
- package/src/styles.mjs +278 -0
- package/src/summary-playback.mjs +39 -0
- package/src/summary.mjs +160 -0
- package/src/terminal.mjs +157 -0
- package/src/tool-detail.mjs +208 -0
- package/src/tool-error.mjs +33 -0
- package/src/tool-identity.mjs +40 -0
- package/src/view.mjs +364 -0
- 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
|
+
}
|
package/src/summary.mjs
ADDED
|
@@ -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
|
+
}
|
package/src/terminal.mjs
ADDED
|
@@ -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
|
+
}
|