clay-server 4.0.0-beta.2 → 4.0.0-beta.3
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/lib/background-task-timing.js +72 -0
- package/lib/public/css/input.css +73 -25
- package/lib/public/modules/background-tasks-ui.js +109 -15
- package/lib/sdk-message-processor.js +10 -2
- package/lib/yoke/adapters/claude.js +6 -1
- package/lib/yoke/codex-background-tasks.js +8 -2
- package/package.json +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Background task start-time tracking.
|
|
2
|
+
//
|
|
3
|
+
// Vendors report background tasks as a full replacement list on every change
|
|
4
|
+
// and none of them carries a reliable start time today (see
|
|
5
|
+
// normalizeBackgroundTasks in yoke/adapters/claude.js and mapTerminals in
|
|
6
|
+
// yoke/codex-background-tasks.js). The session's previous list is the only
|
|
7
|
+
// place that knows when a task was first seen, so the merge belongs here,
|
|
8
|
+
// where that list is maintained, rather than in an adapter that rebuilds the
|
|
9
|
+
// array from scratch each time.
|
|
10
|
+
//
|
|
11
|
+
// A vendor-supplied timestamp always wins when present; otherwise a task is
|
|
12
|
+
// stamped the first time it appears and keeps that stamp for its whole life.
|
|
13
|
+
|
|
14
|
+
// Accepts epoch milliseconds, epoch seconds, or an ISO date string, since
|
|
15
|
+
// vendors are not consistent. Returns null for anything unusable so the
|
|
16
|
+
// caller falls back to first-seen stamping instead of rendering 1970.
|
|
17
|
+
function normalizeTimestamp(value) {
|
|
18
|
+
if (typeof value === "number" && isFinite(value) && value > 0) {
|
|
19
|
+
// Values this small are epoch seconds, not milliseconds.
|
|
20
|
+
return value < 1e12 ? Math.round(value * 1000) : Math.round(value);
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "string" && value) {
|
|
23
|
+
var parsed = Date.parse(value);
|
|
24
|
+
if (!isNaN(parsed)) return parsed;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function vendorStartedAt(task) {
|
|
30
|
+
if (!task) return null;
|
|
31
|
+
return normalizeTimestamp(task.started_at)
|
|
32
|
+
|| normalizeTimestamp(task.startedAt)
|
|
33
|
+
|| normalizeTimestamp(task.createdAt)
|
|
34
|
+
|| normalizeTimestamp(task.created_at);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Return `nextTasks` with a `started_at` (epoch ms) on every entry.
|
|
39
|
+
* Tasks already present in `previousTasks` keep their original stamp so the
|
|
40
|
+
* elapsed time does not reset every time the list is re-emitted.
|
|
41
|
+
*/
|
|
42
|
+
function mergeStartTimes(previousTasks, nextTasks, now) {
|
|
43
|
+
if (!Array.isArray(nextTasks)) return [];
|
|
44
|
+
var stampedAt = typeof now === "number" ? now : Date.now();
|
|
45
|
+
// Stamps we wrote on a previous pass are already epoch milliseconds, so they
|
|
46
|
+
// are read as-is. Only vendor-supplied values go through normalizeTimestamp,
|
|
47
|
+
// whose seconds-vs-milliseconds heuristic would otherwise be re-applied to
|
|
48
|
+
// our own output.
|
|
49
|
+
var known = {};
|
|
50
|
+
var previous = Array.isArray(previousTasks) ? previousTasks : [];
|
|
51
|
+
for (var p = 0; p < previous.length; p++) {
|
|
52
|
+
var seen = previous[p];
|
|
53
|
+
if (!seen || !seen.task_id) continue;
|
|
54
|
+
if (typeof seen.started_at === "number" && isFinite(seen.started_at) && seen.started_at > 0) {
|
|
55
|
+
known[seen.task_id] = seen.started_at;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
var merged = [];
|
|
60
|
+
for (var i = 0; i < nextTasks.length; i++) {
|
|
61
|
+
var task = nextTasks[i];
|
|
62
|
+
if (!task) continue;
|
|
63
|
+
var startedAt = vendorStartedAt(task) || known[task.task_id] || stampedAt;
|
|
64
|
+
merged.push(Object.assign({}, task, { started_at: startedAt }));
|
|
65
|
+
}
|
|
66
|
+
return merged;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
mergeStartTimes: mergeStartTimes,
|
|
71
|
+
normalizeTimestamp: normalizeTimestamp,
|
|
72
|
+
};
|
package/lib/public/css/input.css
CHANGED
|
@@ -667,49 +667,97 @@
|
|
|
667
667
|
position: relative;
|
|
668
668
|
}
|
|
669
669
|
|
|
670
|
+
/* Sidebar session-row badge. Formerly shared its rule with the composer's
|
|
671
|
+
background-tasks chip; it is standalone now that the composer bar no longer
|
|
672
|
+
uses a filled numeric badge. */
|
|
673
|
+
.session-background-task-count {
|
|
674
|
+
display: inline-flex;
|
|
675
|
+
align-items: center;
|
|
676
|
+
justify-content: center;
|
|
677
|
+
min-width: 16px;
|
|
678
|
+
height: 16px;
|
|
679
|
+
padding: 0 4px;
|
|
680
|
+
border-radius: 8px;
|
|
681
|
+
background: var(--accent);
|
|
682
|
+
color: var(--bg);
|
|
683
|
+
font-size: 10px;
|
|
684
|
+
font-weight: 700;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/* --- Background tasks status line (above the composer) ---
|
|
688
|
+
Reads as a calm status line that belongs to the input surface: subtle
|
|
689
|
+
border, no fill, muted text. Progress is conveyed by the animated dots
|
|
690
|
+
rather than by color or size. */
|
|
670
691
|
.background-tasks-bar {
|
|
671
692
|
max-width: var(--content-width);
|
|
672
|
-
margin: 0 auto
|
|
673
|
-
border: 1px solid var(--border);
|
|
674
|
-
border-radius:
|
|
675
|
-
background:
|
|
693
|
+
margin: 0 auto 6px;
|
|
694
|
+
border: 1px solid var(--border-subtle);
|
|
695
|
+
border-radius: 10px;
|
|
696
|
+
background: transparent;
|
|
676
697
|
overflow: hidden;
|
|
698
|
+
transition: border-color .15s ease, background-color .15s ease;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.background-tasks-bar:hover,
|
|
702
|
+
.background-tasks-bar.expanded {
|
|
703
|
+
border-color: var(--border);
|
|
704
|
+
background: var(--input-bg);
|
|
677
705
|
}
|
|
678
706
|
|
|
679
707
|
.background-tasks-toggle {
|
|
680
708
|
width: 100%;
|
|
681
709
|
display: flex;
|
|
682
710
|
align-items: center;
|
|
683
|
-
gap:
|
|
684
|
-
padding:
|
|
711
|
+
gap: 9px;
|
|
712
|
+
padding: 7px 11px;
|
|
685
713
|
border: 0;
|
|
686
714
|
background: transparent;
|
|
687
|
-
color: var(--text-
|
|
715
|
+
color: var(--text-muted);
|
|
688
716
|
cursor: pointer;
|
|
689
717
|
font: inherit;
|
|
718
|
+
font-size: 12px;
|
|
690
719
|
text-align: left;
|
|
691
720
|
}
|
|
692
721
|
|
|
693
|
-
.background-tasks-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
722
|
+
.background-tasks-toggle:hover { color: var(--text-secondary); }
|
|
723
|
+
.background-tasks-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 10px; }
|
|
724
|
+
|
|
725
|
+
.background-tasks-summary { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
726
|
+
|
|
727
|
+
/* Quiet three-dot activity indicator. Same motion as the Home debate dots
|
|
728
|
+
(home-debate-planning.css) but muted instead of accent-colored, since this
|
|
729
|
+
is ambient status rather than something asking to be read. */
|
|
730
|
+
.background-tasks-activity { display: inline-flex; align-items: center; gap: 3px; color: var(--text-muted); flex: none; }
|
|
731
|
+
.background-tasks-activity i { width: 4px; height: 4px; border-radius: 50%; background: currentColor; animation: background-tasks-pulse 1.4s ease-in-out infinite; }
|
|
732
|
+
.background-tasks-activity i:nth-child(2) { animation-delay: .18s; }
|
|
733
|
+
.background-tasks-activity i:nth-child(3) { animation-delay: .36s; }
|
|
734
|
+
@keyframes background-tasks-pulse { 0%, 70%, 100% { opacity: .28; } 35% { opacity: .95; } }
|
|
735
|
+
|
|
736
|
+
.background-tasks-chevron { display: inline-flex; flex: none; color: var(--text-muted); transition: transform .15s ease; }
|
|
737
|
+
.background-tasks-chevron i { width: 14px; height: 14px; }
|
|
738
|
+
.background-tasks-bar.expanded .background-tasks-chevron { transform: rotate(90deg); }
|
|
739
|
+
|
|
740
|
+
.background-tasks-list { padding: 0 11px 7px; }
|
|
741
|
+
.background-task-row { display: flex; align-items: center; gap: 9px; padding: 7px 0; border-top: 1px solid var(--border-subtle); font-size: 12px; }
|
|
742
|
+
.background-task-icon { display: inline-flex; flex: none; color: var(--text-muted); }
|
|
743
|
+
.background-task-icon i { width: 13px; height: 13px; }
|
|
744
|
+
.background-task-description { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-secondary); }
|
|
745
|
+
.background-task-meta { display: inline-flex; align-items: center; gap: 7px; flex: none; color: var(--text-muted); }
|
|
746
|
+
.background-task-type { font-size: 11px; }
|
|
747
|
+
.background-task-elapsed { font-variant-numeric: tabular-nums; font-size: 11px; min-width: 44px; text-align: right; }
|
|
748
|
+
.background-task-stop { flex: none; border: 1px solid var(--border-subtle); border-radius: 6px; padding: 3px 8px; background: transparent; color: var(--text-muted); cursor: pointer; font: inherit; font-size: 11px; }
|
|
749
|
+
.background-task-stop:hover { color: var(--text); border-color: var(--border); }
|
|
750
|
+
.background-task-stop:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
751
|
+
|
|
752
|
+
@media (prefers-reduced-motion: reduce) {
|
|
753
|
+
.background-tasks-activity i { animation: none; opacity: .7; }
|
|
754
|
+
.background-tasks-bar, .background-tasks-chevron { transition: none; }
|
|
705
755
|
}
|
|
706
756
|
|
|
707
|
-
|
|
708
|
-
.background-task-
|
|
709
|
-
.background-task-
|
|
710
|
-
|
|
711
|
-
.background-task-stop { border: 1px solid var(--border); border-radius: 5px; padding: 3px 7px; background: transparent; color: var(--text-secondary); cursor: pointer; font: inherit; font-size: 12px; }
|
|
712
|
-
.background-task-stop:hover { color: var(--text); border-color: var(--text-muted); }
|
|
757
|
+
@media (max-width: 600px) {
|
|
758
|
+
.background-task-meta { gap: 5px; }
|
|
759
|
+
.background-task-type { display: none; }
|
|
760
|
+
}
|
|
713
761
|
|
|
714
762
|
#input-row {
|
|
715
763
|
display: flex;
|
|
@@ -1,55 +1,149 @@
|
|
|
1
|
+
// background-tasks-ui.js - Background task status line above the composer.
|
|
2
|
+
//
|
|
3
|
+
// Collapsed it reads as a quiet status line, not a button demanding attention:
|
|
4
|
+
// a muted activity indicator, a count, and a chevron. Expanding lists each task
|
|
5
|
+
// with its elapsed time and a Stop control.
|
|
6
|
+
//
|
|
7
|
+
// Elapsed time comes from `started_at` (epoch ms) which the server stamps in
|
|
8
|
+
// lib/background-task-timing.js. The ticker only runs while the list is
|
|
9
|
+
// expanded, so a collapsed bar costs nothing.
|
|
10
|
+
|
|
1
11
|
import { store } from './store.js';
|
|
2
12
|
import { getWs } from './ws-ref.js';
|
|
3
13
|
import { escapeHtml } from './utils.js';
|
|
14
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
4
15
|
|
|
5
16
|
var expanded = false;
|
|
17
|
+
var elapsedTimer = null;
|
|
18
|
+
|
|
19
|
+
// Lucide icon per vendor-neutral task type (see normalizeBackgroundTasks in
|
|
20
|
+
// yoke/adapters/claude.js for where these types are assigned).
|
|
21
|
+
var TASK_TYPE_ICONS = { shell: "terminal", agent: "sparkles", other: "circle-dot" };
|
|
6
22
|
|
|
7
23
|
export function initBackgroundTasksUi() {
|
|
8
|
-
store.subscribe(function(state, prev) {
|
|
24
|
+
store.subscribe(function (state, prev) {
|
|
9
25
|
if (state.activeBackgroundTasks !== prev.activeBackgroundTasks) renderBackgroundTasks();
|
|
10
26
|
});
|
|
11
27
|
renderBackgroundTasks();
|
|
12
28
|
}
|
|
13
29
|
|
|
30
|
+
// "45s", "2m 14s", "1h 03m" - short enough to sit in a row without wrapping.
|
|
31
|
+
export function formatElapsed(startedAt, now) {
|
|
32
|
+
if (typeof startedAt !== "number" || !isFinite(startedAt) || startedAt <= 0) return "";
|
|
33
|
+
var current = typeof now === "number" ? now : Date.now();
|
|
34
|
+
var totalSeconds = Math.floor((current - startedAt) / 1000);
|
|
35
|
+
if (totalSeconds < 0) totalSeconds = 0;
|
|
36
|
+
if (totalSeconds < 60) return totalSeconds + "s";
|
|
37
|
+
var minutes = Math.floor(totalSeconds / 60);
|
|
38
|
+
var seconds = totalSeconds % 60;
|
|
39
|
+
if (minutes < 60) return minutes + "m " + pad(seconds) + "s";
|
|
40
|
+
var hours = Math.floor(minutes / 60);
|
|
41
|
+
return hours + "h " + pad(minutes % 60) + "m";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pad(value) {
|
|
45
|
+
return value < 10 ? "0" + value : String(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stopElapsedTimer() {
|
|
49
|
+
if (!elapsedTimer) return;
|
|
50
|
+
clearInterval(elapsedTimer);
|
|
51
|
+
elapsedTimer = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Ticks at most once a second and only touches the time cells, so expanding
|
|
55
|
+
// the list never re-renders (and never steals focus from) the rows.
|
|
56
|
+
function startElapsedTimer() {
|
|
57
|
+
stopElapsedTimer();
|
|
58
|
+
elapsedTimer = setInterval(updateElapsedCells, 1000);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function updateElapsedCells() {
|
|
62
|
+
var bar = document.getElementById('background-tasks-bar');
|
|
63
|
+
if (!bar || !expanded) {
|
|
64
|
+
stopElapsedTimer();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
var now = Date.now();
|
|
68
|
+
var cells = bar.querySelectorAll('.background-task-elapsed');
|
|
69
|
+
for (var i = 0; i < cells.length; i++) {
|
|
70
|
+
var startedAt = Number(cells[i].dataset.startedAt);
|
|
71
|
+
cells[i].textContent = formatElapsed(startedAt, now);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function activityDotsHtml() {
|
|
76
|
+
return '<span class="background-tasks-activity" aria-hidden="true"><i></i><i></i><i></i></span>';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function taskRowHtml(task, now) {
|
|
80
|
+
var taskType = task.task_type || 'other';
|
|
81
|
+
var icon = TASK_TYPE_ICONS[taskType] || TASK_TYPE_ICONS.other;
|
|
82
|
+
var description = task.description || 'Background task';
|
|
83
|
+
var startedAt = typeof task.started_at === 'number' ? task.started_at : 0;
|
|
84
|
+
var elapsed = formatElapsed(startedAt, now);
|
|
85
|
+
return '<div class="background-task-row">' +
|
|
86
|
+
'<span class="background-task-icon" title="' + escapeHtml(taskType) + '">' + iconHtml(icon) + '</span>' +
|
|
87
|
+
'<span class="background-task-description" title="' + escapeHtml(description) + '">' +
|
|
88
|
+
escapeHtml(description) + '</span>' +
|
|
89
|
+
'<span class="background-task-meta">' +
|
|
90
|
+
'<span class="background-task-type">' + escapeHtml(taskType) + '</span>' +
|
|
91
|
+
'<span class="background-task-elapsed" data-started-at="' + startedAt + '">' + escapeHtml(elapsed) + '</span>' +
|
|
92
|
+
'</span>' +
|
|
93
|
+
'<button type="button" class="background-task-stop" data-task-id="' + escapeHtml(task.task_id || '') + '" ' +
|
|
94
|
+
'aria-label="Stop ' + escapeHtml(description) + '">Stop</button>' +
|
|
95
|
+
'</div>';
|
|
96
|
+
}
|
|
97
|
+
|
|
14
98
|
function renderBackgroundTasks() {
|
|
15
99
|
var tasks = store.get('activeBackgroundTasks') || [];
|
|
16
100
|
var existing = document.getElementById('background-tasks-bar');
|
|
17
101
|
if (tasks.length === 0) {
|
|
18
102
|
if (existing) existing.remove();
|
|
19
103
|
expanded = false;
|
|
104
|
+
stopElapsedTimer();
|
|
20
105
|
return;
|
|
21
106
|
}
|
|
22
107
|
var inputArea = document.getElementById('input-area');
|
|
23
108
|
if (!inputArea || !inputArea.parentNode) return;
|
|
109
|
+
|
|
24
110
|
var bar = existing || document.createElement('div');
|
|
25
111
|
bar.id = 'background-tasks-bar';
|
|
26
|
-
bar.className = 'background-tasks-bar';
|
|
112
|
+
bar.className = 'background-tasks-bar' + (expanded ? ' expanded' : '');
|
|
113
|
+
|
|
27
114
|
var taskLabel = tasks.length === 1 ? 'background task' : 'background tasks';
|
|
28
|
-
var
|
|
29
|
-
|
|
30
|
-
'
|
|
115
|
+
var summary = tasks.length + ' ' + taskLabel;
|
|
116
|
+
var html = '<button type="button" class="background-tasks-toggle" aria-expanded="' + expanded + '"' +
|
|
117
|
+
' aria-controls="background-tasks-list" aria-label="' + summary + ', ' +
|
|
118
|
+
(expanded ? 'collapse' : 'expand') + ' details">' +
|
|
119
|
+
activityDotsHtml() +
|
|
120
|
+
'<span class="background-tasks-summary">' + summary + '</span>' +
|
|
121
|
+
'<span class="background-tasks-chevron" aria-hidden="true">' + iconHtml('chevron-right') + '</span>' +
|
|
122
|
+
'</button>';
|
|
123
|
+
|
|
31
124
|
if (expanded) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
html += '<div class="background-task-row"><span class="background-task-description" title="' + escapeHtml(task.description || '') + '">' +
|
|
36
|
-
escapeHtml(task.description || 'Background task') + '</span><span class="background-task-type">' +
|
|
37
|
-
escapeHtml(task.task_type || 'other') + '</span><button type="button" class="background-task-stop" data-task-id="' +
|
|
38
|
-
escapeHtml(task.task_id || '') + '">Stop</button></div>';
|
|
39
|
-
}
|
|
125
|
+
var now = Date.now();
|
|
126
|
+
html += '<div class="background-tasks-list" id="background-tasks-list">';
|
|
127
|
+
for (var i = 0; i < tasks.length; i++) html += taskRowHtml(tasks[i], now);
|
|
40
128
|
html += '</div>';
|
|
41
129
|
}
|
|
42
130
|
bar.innerHTML = html;
|
|
43
131
|
if (!existing) inputArea.parentNode.insertBefore(bar, inputArea);
|
|
44
|
-
|
|
132
|
+
refreshIcons();
|
|
133
|
+
|
|
134
|
+
bar.querySelector('.background-tasks-toggle').addEventListener('click', function () {
|
|
45
135
|
expanded = !expanded;
|
|
46
136
|
renderBackgroundTasks();
|
|
47
137
|
});
|
|
138
|
+
|
|
48
139
|
var stopButtons = bar.querySelectorAll('.background-task-stop');
|
|
49
140
|
for (var j = 0; j < stopButtons.length; j++) {
|
|
50
|
-
stopButtons[j].addEventListener('click', function() {
|
|
141
|
+
stopButtons[j].addEventListener('click', function () {
|
|
51
142
|
var ws = getWs();
|
|
52
143
|
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'stop_task', taskId: this.dataset.taskId }));
|
|
53
144
|
});
|
|
54
145
|
}
|
|
146
|
+
|
|
147
|
+
if (expanded) startElapsedTimer();
|
|
148
|
+
else stopElapsedTimer();
|
|
55
149
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
var usersModule = require("./users");
|
|
2
2
|
var yoke = require("./yoke");
|
|
3
|
+
var backgroundTaskTiming = require("./background-task-timing");
|
|
3
4
|
|
|
4
5
|
function attachMessageProcessor(ctx) {
|
|
5
6
|
var sm = ctx.sm;
|
|
@@ -686,8 +687,15 @@ function attachMessageProcessor(ctx) {
|
|
|
686
687
|
}
|
|
687
688
|
|
|
688
689
|
} else if (parsed.yokeType === "background_tasks_changed") {
|
|
689
|
-
var
|
|
690
|
-
var
|
|
690
|
+
var previousBackgroundTasks = session.activeBackgroundTasks || [];
|
|
691
|
+
var previousBackgroundTaskCount = previousBackgroundTasks.length;
|
|
692
|
+
// Vendors re-send the whole list on every change, so carry each task's
|
|
693
|
+
// first-seen time forward; otherwise the composer's elapsed timer would
|
|
694
|
+
// restart whenever any other task starts or finishes.
|
|
695
|
+
var activeBackgroundTasks = backgroundTaskTiming.mergeStartTimes(
|
|
696
|
+
previousBackgroundTasks,
|
|
697
|
+
Array.isArray(parsed.tasks) ? parsed.tasks : []
|
|
698
|
+
);
|
|
691
699
|
session.activeBackgroundTasks = activeBackgroundTasks;
|
|
692
700
|
sendToSession(session, { type: "active_background_tasks", tasks: activeBackgroundTasks });
|
|
693
701
|
if (previousBackgroundTaskCount !== activeBackgroundTasks.length) {
|
|
@@ -400,11 +400,16 @@ function normalizeBackgroundTasks(tasks) {
|
|
|
400
400
|
var nativeType = task && task.task_type;
|
|
401
401
|
var taskType = nativeType === "local_bash" ? "shell"
|
|
402
402
|
: nativeType === "local_agent" ? "agent" : "other";
|
|
403
|
-
|
|
403
|
+
var normalized = {
|
|
404
404
|
task_id: task && task.task_id,
|
|
405
405
|
task_type: taskType,
|
|
406
406
|
description: (task && task.description) || "",
|
|
407
407
|
};
|
|
408
|
+
// The SDK does not report a start time today. Forward one if a future
|
|
409
|
+
// version adds it; otherwise the session stamps first-seen time.
|
|
410
|
+
var startedAt = task && (task.started_at || task.startedAt || task.created_at);
|
|
411
|
+
if (startedAt) normalized.started_at = startedAt;
|
|
412
|
+
return normalized;
|
|
408
413
|
});
|
|
409
414
|
}
|
|
410
415
|
|
|
@@ -19,11 +19,17 @@ function mapTerminals(result) {
|
|
|
19
19
|
var terminal = terminals[i] || {};
|
|
20
20
|
var taskId = terminal.id || terminal.terminalId || terminal.taskId || "";
|
|
21
21
|
if (!taskId || !isLiveTerminal(terminal)) continue;
|
|
22
|
-
|
|
22
|
+
var task = {
|
|
23
23
|
task_id: String(taskId),
|
|
24
24
|
task_type: "shell",
|
|
25
25
|
description: terminal.commandLine || terminal.command || terminal.name || "",
|
|
26
|
-
}
|
|
26
|
+
};
|
|
27
|
+
// Forward the terminal's own start time when the app-server reports one,
|
|
28
|
+
// so elapsed time survives a Clay restart. Falls back to first-seen
|
|
29
|
+
// stamping in background-task-timing.js when absent.
|
|
30
|
+
var startedAt = terminal.startedAt || terminal.started_at || terminal.createdAt || terminal.created_at;
|
|
31
|
+
if (startedAt) task.started_at = startedAt;
|
|
32
|
+
tasks.push(task);
|
|
27
33
|
}
|
|
28
34
|
return tasks;
|
|
29
35
|
}
|
package/package.json
CHANGED