palmier 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/commands/run.js +55 -0
- package/dist/commands/serve.js +22 -2
- package/dist/event-queues.d.ts +36 -0
- package/dist/event-queues.js +53 -0
- package/dist/mcp-tools.d.ts +2 -0
- package/dist/mcp-tools.js +4 -2
- package/dist/platform/linux.js +11 -8
- package/dist/platform/windows.d.ts +5 -6
- package/dist/platform/windows.js +19 -13
- package/dist/pwa/assets/index-FP1Mipr6.js +120 -0
- package/dist/pwa/assets/index-bLTn8zBj.css +1 -0
- package/dist/pwa/assets/{web-BNr628AV.js → web-BpM3fNCn.js} +1 -1
- package/dist/pwa/assets/{web-DyQPewAi.js → web-CF-N8Di6.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +25 -9
- package/dist/task.js +1 -1
- package/dist/transports/http-transport.js +18 -5
- package/dist/types.d.ts +10 -6
- package/package.json +1 -1
- package/palmier-server/README.md +3 -3
- package/palmier-server/pwa/src/App.css +117 -36
- package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +46 -0
- package/palmier-server/pwa/src/components/RunDetailView.tsx +58 -15
- package/palmier-server/pwa/src/components/SessionComposer.tsx +20 -10
- package/palmier-server/pwa/src/components/{RunsView.tsx → SessionsView.tsx} +33 -25
- package/palmier-server/pwa/src/components/TaskCard.tsx +33 -35
- package/palmier-server/pwa/src/components/TaskForm.tsx +274 -293
- package/palmier-server/pwa/src/components/{TaskListView.tsx → TasksView.tsx} +20 -13
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +16 -8
- package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +102 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +9 -26
- package/palmier-server/pwa/src/types.ts +5 -9
- package/palmier-server/spec.md +23 -23
- package/src/commands/run.ts +61 -0
- package/src/commands/serve.ts +22 -2
- package/src/event-queues.ts +56 -0
- package/src/mcp-tools.ts +6 -2
- package/src/platform/linux.ts +10 -8
- package/src/platform/windows.ts +19 -13
- package/src/rpc-handler.ts +28 -11
- package/src/task.ts +1 -1
- package/src/transports/http-transport.ts +17 -5
- package/src/types.ts +10 -7
- package/dist/pwa/assets/index-8cTctVnD.js +0 -120
- package/dist/pwa/assets/index-CSUkBBsQ.css +0 -1
|
@@ -67,6 +67,34 @@ body {
|
|
|
67
67
|
min-height: 100dvh;
|
|
68
68
|
-webkit-font-smoothing: antialiased;
|
|
69
69
|
-moz-osx-font-smoothing: grayscale;
|
|
70
|
+
overscroll-behavior-y: contain;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.pull-to-refresh {
|
|
74
|
+
position: fixed;
|
|
75
|
+
top: -48px;
|
|
76
|
+
left: 0;
|
|
77
|
+
right: 0;
|
|
78
|
+
height: 48px;
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: center;
|
|
82
|
+
pointer-events: none;
|
|
83
|
+
z-index: 40;
|
|
84
|
+
transition: opacity 0.15s;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.pull-to-refresh-badge {
|
|
88
|
+
width: 40px;
|
|
89
|
+
height: 40px;
|
|
90
|
+
border-radius: 50%;
|
|
91
|
+
background: var(--color-surface);
|
|
92
|
+
border: 1px solid var(--color-border);
|
|
93
|
+
box-shadow: var(--shadow-md);
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
justify-content: center;
|
|
97
|
+
color: var(--color-text-secondary);
|
|
70
98
|
}
|
|
71
99
|
|
|
72
100
|
#root {
|
|
@@ -621,10 +649,6 @@ body {
|
|
|
621
649
|
padding-left: var(--space-xs);
|
|
622
650
|
}
|
|
623
651
|
|
|
624
|
-
.session-composer-controls .agent-picker-section-inline {
|
|
625
|
-
margin-left: 0;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
652
|
.session-composer-yolo {
|
|
629
653
|
display: inline-flex;
|
|
630
654
|
align-items: center;
|
|
@@ -645,23 +669,39 @@ body {
|
|
|
645
669
|
}
|
|
646
670
|
|
|
647
671
|
/* ===== Task List ===== */
|
|
648
|
-
.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
672
|
+
.fab {
|
|
673
|
+
position: fixed;
|
|
674
|
+
right: var(--space-lg);
|
|
675
|
+
bottom: calc(var(--space-lg) + env(safe-area-inset-bottom, 0px));
|
|
676
|
+
width: 56px;
|
|
677
|
+
height: 56px;
|
|
678
|
+
border-radius: 50%;
|
|
679
|
+
border: none;
|
|
680
|
+
background: var(--color-primary);
|
|
681
|
+
color: #fff;
|
|
682
|
+
display: inline-flex;
|
|
683
|
+
align-items: center;
|
|
684
|
+
justify-content: center;
|
|
655
685
|
cursor: pointer;
|
|
686
|
+
box-shadow: var(--shadow-lg);
|
|
687
|
+
transition: background var(--transition-base), transform var(--transition-fast), box-shadow var(--transition-base);
|
|
688
|
+
z-index: 50;
|
|
689
|
+
-webkit-tap-highlight-color: transparent;
|
|
656
690
|
}
|
|
657
691
|
|
|
658
|
-
.
|
|
659
|
-
|
|
692
|
+
.fab:hover {
|
|
693
|
+
background: var(--color-primary-hover);
|
|
694
|
+
box-shadow: var(--shadow-xl);
|
|
695
|
+
transform: translateY(-1px);
|
|
660
696
|
}
|
|
661
697
|
|
|
662
|
-
.
|
|
663
|
-
|
|
664
|
-
|
|
698
|
+
.fab:active {
|
|
699
|
+
transform: translateY(0);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.fab:focus-visible {
|
|
703
|
+
outline: 2px solid var(--color-primary);
|
|
704
|
+
outline-offset: 3px;
|
|
665
705
|
}
|
|
666
706
|
|
|
667
707
|
.section-label {
|
|
@@ -677,7 +717,6 @@ body {
|
|
|
677
717
|
display: flex;
|
|
678
718
|
align-items: center;
|
|
679
719
|
gap: var(--space-xs);
|
|
680
|
-
margin-left: auto;
|
|
681
720
|
}
|
|
682
721
|
|
|
683
722
|
.agent-picker-section-inline .agent-picker-label {
|
|
@@ -1162,15 +1201,57 @@ body {
|
|
|
1162
1201
|
font-weight: 600;
|
|
1163
1202
|
}
|
|
1164
1203
|
|
|
1165
|
-
.
|
|
1204
|
+
.granted-permissions-row {
|
|
1205
|
+
flex-basis: 100%;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.granted-permissions-row .btn-link {
|
|
1209
|
+
padding: 0;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
.schedule-section {
|
|
1166
1213
|
display: flex;
|
|
1167
1214
|
flex-direction: column;
|
|
1168
|
-
gap:
|
|
1215
|
+
gap: var(--space-sm);
|
|
1169
1216
|
}
|
|
1170
1217
|
|
|
1171
|
-
.
|
|
1172
|
-
|
|
1173
|
-
|
|
1218
|
+
.schedule-section-title {
|
|
1219
|
+
font-size: 0.75rem;
|
|
1220
|
+
font-weight: 600;
|
|
1221
|
+
text-transform: uppercase;
|
|
1222
|
+
letter-spacing: 0.05em;
|
|
1223
|
+
color: var(--color-muted);
|
|
1224
|
+
margin: 0;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
.schedule-reactive {
|
|
1228
|
+
display: flex;
|
|
1229
|
+
flex-direction: column;
|
|
1230
|
+
gap: var(--space-xs);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
.yolo-inline {
|
|
1234
|
+
display: inline-flex;
|
|
1235
|
+
align-items: center;
|
|
1236
|
+
gap: 4px;
|
|
1237
|
+
font-size: 0.8rem;
|
|
1238
|
+
color: var(--color-text-secondary);
|
|
1239
|
+
cursor: pointer;
|
|
1240
|
+
user-select: none;
|
|
1241
|
+
white-space: nowrap;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.yolo-inline input {
|
|
1245
|
+
margin: 0;
|
|
1246
|
+
cursor: pointer;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
.yolo-warning {
|
|
1250
|
+
flex-basis: 100%;
|
|
1251
|
+
margin: 0;
|
|
1252
|
+
font-size: 0.75rem;
|
|
1253
|
+
color: var(--color-text-secondary);
|
|
1254
|
+
line-height: 1.4;
|
|
1174
1255
|
}
|
|
1175
1256
|
|
|
1176
1257
|
.trigger-row-card {
|
|
@@ -1218,7 +1299,7 @@ body {
|
|
|
1218
1299
|
width: auto;
|
|
1219
1300
|
}
|
|
1220
1301
|
|
|
1221
|
-
.
|
|
1302
|
+
.schedule-section > .form-select,
|
|
1222
1303
|
.trigger-row-card .form-select,
|
|
1223
1304
|
.trigger-row-card .form-input {
|
|
1224
1305
|
margin-bottom: 0;
|
|
@@ -1229,7 +1310,7 @@ body {
|
|
|
1229
1310
|
min-width: 0;
|
|
1230
1311
|
}
|
|
1231
1312
|
|
|
1232
|
-
.
|
|
1313
|
+
.schedule-section > .form-select {
|
|
1233
1314
|
width: 100%;
|
|
1234
1315
|
}
|
|
1235
1316
|
|
|
@@ -1921,9 +2002,9 @@ body {
|
|
|
1921
2002
|
border-bottom-color: var(--color-primary);
|
|
1922
2003
|
}
|
|
1923
2004
|
|
|
1924
|
-
/* =====
|
|
2005
|
+
/* ===== Sessions view ===== */
|
|
1925
2006
|
|
|
1926
|
-
.
|
|
2007
|
+
.sessions-view {
|
|
1927
2008
|
flex: 1;
|
|
1928
2009
|
display: flex;
|
|
1929
2010
|
flex-direction: column;
|
|
@@ -1931,7 +2012,7 @@ body {
|
|
|
1931
2012
|
justify-content: center;
|
|
1932
2013
|
}
|
|
1933
2014
|
|
|
1934
|
-
.
|
|
2015
|
+
.sessions-card {
|
|
1935
2016
|
background: var(--color-surface);
|
|
1936
2017
|
border-radius: var(--radius-md);
|
|
1937
2018
|
border: 1px solid var(--color-border);
|
|
@@ -1945,12 +2026,12 @@ body {
|
|
|
1945
2026
|
-webkit-tap-highlight-color: transparent;
|
|
1946
2027
|
}
|
|
1947
2028
|
|
|
1948
|
-
.
|
|
2029
|
+
.sessions-card:hover {
|
|
1949
2030
|
box-shadow: var(--shadow-md);
|
|
1950
2031
|
border-color: #CBD5E1;
|
|
1951
2032
|
}
|
|
1952
2033
|
|
|
1953
|
-
.
|
|
2034
|
+
.sessions-card-body {
|
|
1954
2035
|
flex: 1;
|
|
1955
2036
|
min-width: 0;
|
|
1956
2037
|
display: flex;
|
|
@@ -1958,7 +2039,7 @@ body {
|
|
|
1958
2039
|
gap: var(--space-xs);
|
|
1959
2040
|
}
|
|
1960
2041
|
|
|
1961
|
-
.
|
|
2042
|
+
.sessions-card-name {
|
|
1962
2043
|
font-size: 0.9375rem;
|
|
1963
2044
|
font-weight: 600;
|
|
1964
2045
|
letter-spacing: -0.01em;
|
|
@@ -1968,7 +2049,7 @@ body {
|
|
|
1968
2049
|
text-overflow: ellipsis;
|
|
1969
2050
|
}
|
|
1970
2051
|
|
|
1971
|
-
.
|
|
2052
|
+
.sessions-card-meta {
|
|
1972
2053
|
display: flex;
|
|
1973
2054
|
flex-wrap: wrap;
|
|
1974
2055
|
gap: var(--space-sm);
|
|
@@ -1976,7 +2057,7 @@ body {
|
|
|
1976
2057
|
color: var(--color-text-secondary);
|
|
1977
2058
|
}
|
|
1978
2059
|
|
|
1979
|
-
.
|
|
2060
|
+
.sessions-card-chevron {
|
|
1980
2061
|
flex-shrink: 0;
|
|
1981
2062
|
align-self: center;
|
|
1982
2063
|
color: var(--color-text-secondary);
|
|
@@ -1986,11 +2067,11 @@ body {
|
|
|
1986
2067
|
transition: opacity var(--transition-base);
|
|
1987
2068
|
}
|
|
1988
2069
|
|
|
1989
|
-
.
|
|
2070
|
+
.sessions-card:hover .sessions-card-chevron {
|
|
1990
2071
|
opacity: 0.8;
|
|
1991
2072
|
}
|
|
1992
2073
|
|
|
1993
|
-
.
|
|
2074
|
+
.sessions-filter-chip {
|
|
1994
2075
|
display: inline-flex;
|
|
1995
2076
|
align-items: center;
|
|
1996
2077
|
gap: var(--space-xs);
|
|
@@ -2002,7 +2083,7 @@ body {
|
|
|
2002
2083
|
border-radius: 999px;
|
|
2003
2084
|
}
|
|
2004
2085
|
|
|
2005
|
-
.
|
|
2086
|
+
.sessions-filter-chip button {
|
|
2006
2087
|
display: inline-flex;
|
|
2007
2088
|
align-items: center;
|
|
2008
2089
|
justify-content: center;
|
|
@@ -2015,7 +2096,7 @@ body {
|
|
|
2015
2096
|
line-height: 1;
|
|
2016
2097
|
}
|
|
2017
2098
|
|
|
2018
|
-
.
|
|
2099
|
+
.sessions-filter-chip button:hover {
|
|
2019
2100
|
color: var(--color-text);
|
|
2020
2101
|
}
|
|
2021
2102
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
pullDistance: number;
|
|
3
|
+
refreshing: boolean;
|
|
4
|
+
threshold: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fixed-position indicator parked just above the viewport that slides down as
|
|
9
|
+
* the user pulls. Opacity ramps up quickly so the badge peeks in immediately;
|
|
10
|
+
* the arrow flips upward once the pull has crossed the release threshold.
|
|
11
|
+
*/
|
|
12
|
+
export default function PullToRefreshIndicator({ pullDistance, refreshing, threshold }: Props) {
|
|
13
|
+
const visible = pullDistance > 0 || refreshing;
|
|
14
|
+
const ready = pullDistance >= threshold;
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className="pull-to-refresh"
|
|
18
|
+
style={{
|
|
19
|
+
transform: `translateY(${pullDistance}px)`,
|
|
20
|
+
opacity: visible ? Math.min(1, pullDistance / 30) : 0,
|
|
21
|
+
}}
|
|
22
|
+
aria-hidden={!visible}
|
|
23
|
+
>
|
|
24
|
+
<div className="pull-to-refresh-badge">
|
|
25
|
+
{refreshing ? (
|
|
26
|
+
<div className="spinner" />
|
|
27
|
+
) : (
|
|
28
|
+
<svg
|
|
29
|
+
width="18"
|
|
30
|
+
height="18"
|
|
31
|
+
viewBox="0 0 24 24"
|
|
32
|
+
fill="none"
|
|
33
|
+
stroke="currentColor"
|
|
34
|
+
strokeWidth="2.25"
|
|
35
|
+
strokeLinecap="round"
|
|
36
|
+
strokeLinejoin="round"
|
|
37
|
+
style={{ transform: ready ? "rotate(180deg)" : "rotate(0deg)", transition: "transform 0.15s" }}
|
|
38
|
+
>
|
|
39
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
40
|
+
<polyline points="19 12 12 19 5 12" />
|
|
41
|
+
</svg>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -30,8 +30,33 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
|
|
|
30
30
|
const [followupText, setFollowupText] = useState("");
|
|
31
31
|
const [sendingFollowup, setSendingFollowup] = useState(false);
|
|
32
32
|
const threadRef = useRef<HTMLDivElement>(null);
|
|
33
|
+
const followupInputRef = useRef<HTMLInputElement>(null);
|
|
34
|
+
// Tracks which runId we've already scrolled-and-focused for, so new messages
|
|
35
|
+
// during the session don't steal focus back to the input.
|
|
36
|
+
const initialFocusForRunId = useRef<string | null>(null);
|
|
37
|
+
|
|
38
|
+
// Resolve the "latest" sentinel to an actual run_id. `undefined` = still
|
|
39
|
+
// resolving, `null` = task has no runs yet (empty state), otherwise the id
|
|
40
|
+
// to load.
|
|
41
|
+
const [resolvedRunId, setResolvedRunId] = useState<string | null | undefined>(
|
|
42
|
+
runId === "latest" ? undefined : runId,
|
|
43
|
+
);
|
|
44
|
+
const isLatestEmpty = runId === "latest" && resolvedRunId === null;
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (runId !== "latest") {
|
|
48
|
+
setResolvedRunId(runId);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!connected) return;
|
|
52
|
+
setResolvedRunId(undefined);
|
|
53
|
+
request<{ entries?: Array<{ run_id: string }> }>("taskrun.list", { task_id: taskId, limit: 1 })
|
|
54
|
+
.then((result) => setResolvedRunId(result.entries?.[0]?.run_id ?? null))
|
|
55
|
+
.catch(() => setResolvedRunId(null));
|
|
56
|
+
}, [runId, taskId, connected, request]);
|
|
33
57
|
|
|
34
58
|
async function fetchData() {
|
|
59
|
+
if (!resolvedRunId) return;
|
|
35
60
|
try {
|
|
36
61
|
const result = await request<{
|
|
37
62
|
messages?: ConversationMessage[];
|
|
@@ -40,7 +65,7 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
|
|
|
40
65
|
running_state?: string;
|
|
41
66
|
agent?: string;
|
|
42
67
|
error?: string;
|
|
43
|
-
}>("task.result", { id: taskId, run_id:
|
|
68
|
+
}>("task.result", { id: taskId, run_id: resolvedRunId });
|
|
44
69
|
|
|
45
70
|
if (result.error) {
|
|
46
71
|
console.error("No result:", result.error);
|
|
@@ -59,9 +84,10 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
|
|
|
59
84
|
}
|
|
60
85
|
|
|
61
86
|
async function openReport(file: string) {
|
|
87
|
+
if (!resolvedRunId) return;
|
|
62
88
|
try {
|
|
63
89
|
const result = await request<{ reports: Array<{ file: string; content?: string; data_url?: string }> }>(
|
|
64
|
-
"task.reports", { id: taskId, run_id:
|
|
90
|
+
"task.reports", { id: taskId, run_id: resolvedRunId, report_files: [file] },
|
|
65
91
|
);
|
|
66
92
|
const report = result.reports?.[0];
|
|
67
93
|
if (report?.data_url) {
|
|
@@ -74,30 +100,28 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
|
|
|
74
100
|
}
|
|
75
101
|
}
|
|
76
102
|
|
|
77
|
-
// Initial load
|
|
103
|
+
// Initial load once resolvedRunId becomes a concrete id.
|
|
78
104
|
useEffect(() => {
|
|
79
|
-
if (connected)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}, [connected, taskId, runId]);
|
|
105
|
+
if (!connected || !resolvedRunId) return;
|
|
106
|
+
setLoading(true);
|
|
107
|
+
fetchData();
|
|
108
|
+
}, [connected, taskId, resolvedRunId]);
|
|
84
109
|
|
|
85
110
|
// Live-update when the viewed task's state changes
|
|
86
111
|
useEffect(() => {
|
|
87
|
-
if (!connected || !hostId) return;
|
|
112
|
+
if (!connected || !hostId || !resolvedRunId) return;
|
|
88
113
|
const unsubscribe = subscribeEvents(hostId, async (msg) => {
|
|
89
114
|
try {
|
|
90
115
|
const parsed = JSON.parse(new TextDecoder().decode(msg.data)) as { event_type?: string; run_id?: string };
|
|
91
116
|
if (parsed.event_type !== "running-state" && parsed.event_type !== "result-updated") return;
|
|
92
117
|
const eventTaskId = msg.subject.split(".").pop();
|
|
93
118
|
if (eventTaskId !== taskId) return;
|
|
94
|
-
|
|
95
|
-
if (parsed.event_type === "result-updated" && parsed.run_id && parsed.run_id !== runId) return;
|
|
119
|
+
if (parsed.event_type === "result-updated" && parsed.run_id && parsed.run_id !== resolvedRunId) return;
|
|
96
120
|
fetchData();
|
|
97
121
|
} catch { /* skip */ }
|
|
98
122
|
});
|
|
99
123
|
return unsubscribe;
|
|
100
|
-
}, [connected, hostId, taskId, subscribeEvents, request]);
|
|
124
|
+
}, [connected, hostId, taskId, resolvedRunId, subscribeEvents, request]);
|
|
101
125
|
|
|
102
126
|
// Auto-scroll to bottom when messages change
|
|
103
127
|
useEffect(() => {
|
|
@@ -106,6 +130,19 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
|
|
|
106
130
|
}
|
|
107
131
|
}, [messages]);
|
|
108
132
|
|
|
133
|
+
// On first load of a run, scroll the window to the bottom so the follow-up
|
|
134
|
+
// input is visible, and focus the input if it's rendered (agent not running).
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (loading || isLatestEmpty || !resolvedRunId) return;
|
|
137
|
+
if (initialFocusForRunId.current === resolvedRunId) return;
|
|
138
|
+
initialFocusForRunId.current = resolvedRunId;
|
|
139
|
+
requestAnimationFrame(() => {
|
|
140
|
+
if (threadRef.current) threadRef.current.scrollTop = threadRef.current.scrollHeight;
|
|
141
|
+
window.scrollTo({ top: document.documentElement.scrollHeight, behavior: "auto" });
|
|
142
|
+
if (!isAgentGenerating) followupInputRef.current?.focus();
|
|
143
|
+
});
|
|
144
|
+
}, [loading, isLatestEmpty, resolvedRunId, isAgentGenerating]);
|
|
145
|
+
|
|
109
146
|
function typeLabel(type?: string): string | undefined {
|
|
110
147
|
if (type === "input") return "User Input";
|
|
111
148
|
if (type === "permission") return "Permission";
|
|
@@ -133,7 +170,12 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
|
|
|
133
170
|
Back
|
|
134
171
|
</button>
|
|
135
172
|
|
|
136
|
-
{
|
|
173
|
+
{isLatestEmpty ? (
|
|
174
|
+
<div className="empty-state">
|
|
175
|
+
<p className="empty-state-text">No runs yet</p>
|
|
176
|
+
<p className="empty-state-hint">This task hasn't been executed yet. Run it from the task menu or wait for its next trigger.</p>
|
|
177
|
+
</div>
|
|
178
|
+
) : loading ? (
|
|
137
179
|
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)", padding: "var(--space-sm) 0" }}>
|
|
138
180
|
<div className="skeleton-line" style={{ width: "40%" }} />
|
|
139
181
|
<div className="skeleton-line" style={{ width: "55%" }} />
|
|
@@ -223,7 +265,7 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
|
|
|
223
265
|
onClick={async () => {
|
|
224
266
|
setAborting(true);
|
|
225
267
|
try {
|
|
226
|
-
await request("task.stop_followup", { id: taskId, run_id:
|
|
268
|
+
await request("task.stop_followup", { id: taskId, run_id: resolvedRunId });
|
|
227
269
|
} catch (err) {
|
|
228
270
|
console.error("Stop failed:", err);
|
|
229
271
|
} finally {
|
|
@@ -243,7 +285,7 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
|
|
|
243
285
|
if (!msg || sendingFollowup) return;
|
|
244
286
|
setSendingFollowup(true);
|
|
245
287
|
try {
|
|
246
|
-
await request("task.followup", { id: taskId, run_id:
|
|
288
|
+
await request("task.followup", { id: taskId, run_id: resolvedRunId, message: msg });
|
|
247
289
|
setFollowupText("");
|
|
248
290
|
} catch (err) {
|
|
249
291
|
console.error("Follow-up failed:", err);
|
|
@@ -253,6 +295,7 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
|
|
|
253
295
|
}}
|
|
254
296
|
>
|
|
255
297
|
<input
|
|
298
|
+
ref={followupInputRef}
|
|
256
299
|
className="chat-input"
|
|
257
300
|
type="text"
|
|
258
301
|
placeholder="Follow-up message"
|
|
@@ -38,6 +38,14 @@ export default function SessionComposer({ agents, onStarted }: SessionComposerPr
|
|
|
38
38
|
return () => setDraftMessage(null);
|
|
39
39
|
}, [prompt]);
|
|
40
40
|
|
|
41
|
+
const selectedAgent = agents.find((a) => a.key === agent);
|
|
42
|
+
const agentSupportsYolo = !!selectedAgent?.supportsYolo;
|
|
43
|
+
|
|
44
|
+
// Force-disable yolo when the selected agent doesn't support it.
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!agentSupportsYolo && yoloMode) setYoloMode(false);
|
|
47
|
+
}, [agentSupportsYolo, yoloMode]);
|
|
48
|
+
|
|
41
49
|
const canRun = !!prompt.trim() && !!agent && !running;
|
|
42
50
|
|
|
43
51
|
function confirmYolo(): boolean {
|
|
@@ -104,15 +112,17 @@ export default function SessionComposer({ agents, onStarted }: SessionComposerPr
|
|
|
104
112
|
))}
|
|
105
113
|
</select>
|
|
106
114
|
</div>
|
|
107
|
-
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
{agentSupportsYolo && (
|
|
116
|
+
<label className="session-composer-yolo">
|
|
117
|
+
<input
|
|
118
|
+
type="checkbox"
|
|
119
|
+
checked={yoloMode}
|
|
120
|
+
onChange={(e) => setYoloMode(e.target.checked)}
|
|
121
|
+
disabled={running}
|
|
122
|
+
/>
|
|
123
|
+
Yolo
|
|
124
|
+
</label>
|
|
125
|
+
)}
|
|
116
126
|
<button
|
|
117
127
|
className="btn btn-primary chat-send-btn"
|
|
118
128
|
onClick={handleRun}
|
|
@@ -127,7 +137,7 @@ export default function SessionComposer({ agents, onStarted }: SessionComposerPr
|
|
|
127
137
|
)}
|
|
128
138
|
</button>
|
|
129
139
|
</div>
|
|
130
|
-
{yoloMode && (
|
|
140
|
+
{agentSupportsYolo && yoloMode && (
|
|
131
141
|
<p className="command-help-text">
|
|
132
142
|
The agent will auto-approve all tool calls without asking for permission.
|
|
133
143
|
</p>
|
|
@@ -3,9 +3,11 @@ import { useNavigate } from "react-router-dom";
|
|
|
3
3
|
import { formatTime } from "../formatTime";
|
|
4
4
|
import { confirmLeaveDraft } from "../draftGuard";
|
|
5
5
|
import SessionComposer from "./SessionComposer";
|
|
6
|
+
import PullToRefreshIndicator from "./PullToRefreshIndicator";
|
|
7
|
+
import { usePullToRefresh } from "../hooks/usePullToRefresh";
|
|
6
8
|
import type { AgentInfo, HistoryEntry } from "../types";
|
|
7
9
|
|
|
8
|
-
interface
|
|
10
|
+
interface SessionsViewProps {
|
|
9
11
|
connected: boolean;
|
|
10
12
|
hostId: string | null;
|
|
11
13
|
request<T = unknown>(method: string, params?: unknown, opts?: { timeout?: number }): Promise<T>;
|
|
@@ -17,7 +19,7 @@ interface RunsViewProps {
|
|
|
17
19
|
|
|
18
20
|
const PAGE_SIZE = 10;
|
|
19
21
|
|
|
20
|
-
export default function
|
|
22
|
+
export default function SessionsView({ connected, hostId, request, subscribeEvents, agents, filterTaskId, onClearFilter }: SessionsViewProps) {
|
|
21
23
|
const [entries, setEntries] = useState<HistoryEntry[]>([]);
|
|
22
24
|
const [total, setTotal] = useState(0);
|
|
23
25
|
const [loading, setLoading] = useState(false);
|
|
@@ -76,6 +78,11 @@ export default function RunsView({ connected, hostId, request, subscribeEvents,
|
|
|
76
78
|
}
|
|
77
79
|
}, [connected, hostId, loadHistory, filterTaskId]);
|
|
78
80
|
|
|
81
|
+
const ptr = usePullToRefresh({
|
|
82
|
+
onRefresh: () => loadHistory(0, false),
|
|
83
|
+
enabled: connected,
|
|
84
|
+
});
|
|
85
|
+
|
|
79
86
|
// Real-time: update entries on running-state events
|
|
80
87
|
useEffect(() => {
|
|
81
88
|
if (!connected || !hostId) return;
|
|
@@ -156,6 +163,17 @@ export default function RunsView({ connected, hostId, request, subscribeEvents,
|
|
|
156
163
|
}}
|
|
157
164
|
/>
|
|
158
165
|
);
|
|
166
|
+
const refreshIndicator = (
|
|
167
|
+
<PullToRefreshIndicator pullDistance={ptr.pullDistance} refreshing={ptr.refreshing} threshold={ptr.threshold} />
|
|
168
|
+
);
|
|
169
|
+
const filterChip = filterTaskId && onClearFilter && (
|
|
170
|
+
<div style={{ marginBottom: "var(--space-sm)" }}>
|
|
171
|
+
<span className="sessions-filter-chip">
|
|
172
|
+
Filtered by task
|
|
173
|
+
<button onClick={onClearFilter} aria-label="Clear filter">×</button>
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
159
177
|
|
|
160
178
|
function stateColor(state?: string): string | undefined {
|
|
161
179
|
if (state === "failed") return "var(--color-error)";
|
|
@@ -167,6 +185,7 @@ export default function RunsView({ connected, hostId, request, subscribeEvents,
|
|
|
167
185
|
if (loading && entries.length === 0 && connected) {
|
|
168
186
|
return (
|
|
169
187
|
<>
|
|
188
|
+
{refreshIndicator}
|
|
170
189
|
{composer}
|
|
171
190
|
<div className="task-list">
|
|
172
191
|
{[0, 1, 2].map((i) => (
|
|
@@ -190,8 +209,9 @@ export default function RunsView({ connected, hostId, request, subscribeEvents,
|
|
|
190
209
|
if (!connected || (loading && entries.length === 0)) {
|
|
191
210
|
return (
|
|
192
211
|
<>
|
|
212
|
+
{refreshIndicator}
|
|
193
213
|
{composer}
|
|
194
|
-
<div className="
|
|
214
|
+
<div className="sessions-view">
|
|
195
215
|
<div className="empty-state">
|
|
196
216
|
<p className="empty-state-text">Sessions</p>
|
|
197
217
|
<p className="empty-state-hint">Your sessions will appear here</p>
|
|
@@ -204,16 +224,10 @@ export default function RunsView({ connected, hostId, request, subscribeEvents,
|
|
|
204
224
|
if (entries.length === 0) {
|
|
205
225
|
return (
|
|
206
226
|
<>
|
|
227
|
+
{refreshIndicator}
|
|
207
228
|
{composer}
|
|
208
|
-
{
|
|
209
|
-
|
|
210
|
-
<span className="runs-filter-chip">
|
|
211
|
-
Filtered by task
|
|
212
|
-
<button onClick={onClearFilter} aria-label="Clear filter">×</button>
|
|
213
|
-
</span>
|
|
214
|
-
</div>
|
|
215
|
-
)}
|
|
216
|
-
<div className="runs-view">
|
|
229
|
+
{filterChip}
|
|
230
|
+
<div className="sessions-view">
|
|
217
231
|
<div className="empty-state">
|
|
218
232
|
<p className="empty-state-text">No sessions yet</p>
|
|
219
233
|
<p className="empty-state-hint">
|
|
@@ -229,25 +243,19 @@ export default function RunsView({ connected, hostId, request, subscribeEvents,
|
|
|
229
243
|
|
|
230
244
|
return (
|
|
231
245
|
<>
|
|
246
|
+
{refreshIndicator}
|
|
232
247
|
{composer}
|
|
233
|
-
{
|
|
234
|
-
<div style={{ marginBottom: "var(--space-sm)" }}>
|
|
235
|
-
<span className="runs-filter-chip">
|
|
236
|
-
Filtered by task
|
|
237
|
-
<button onClick={onClearFilter} aria-label="Clear filter">×</button>
|
|
238
|
-
</span>
|
|
239
|
-
</div>
|
|
240
|
-
)}
|
|
248
|
+
{filterChip}
|
|
241
249
|
<div className="task-list">
|
|
242
250
|
{entries.map((entry, i) => (
|
|
243
251
|
<div
|
|
244
252
|
key={`${entry.task_id}-${entry.run_id}-${i}`}
|
|
245
|
-
className="
|
|
253
|
+
className="sessions-card"
|
|
246
254
|
onClick={() => !entry.error && handleCardClick(entry.task_id, entry.run_id)}
|
|
247
255
|
>
|
|
248
|
-
<div className="
|
|
249
|
-
<h3 className="
|
|
250
|
-
<div className="
|
|
256
|
+
<div className="sessions-card-body">
|
|
257
|
+
<h3 className="sessions-card-name">{entry.task_name || entry.task_id}</h3>
|
|
258
|
+
<div className="sessions-card-meta">
|
|
251
259
|
<span style={{ color: stateColor(entry.running_state) }}>
|
|
252
260
|
{stateLabel[entry.running_state ?? ""] ?? entry.running_state}
|
|
253
261
|
</span>
|
|
@@ -262,7 +270,7 @@ export default function RunsView({ connected, hostId, request, subscribeEvents,
|
|
|
262
270
|
)}
|
|
263
271
|
</div>
|
|
264
272
|
</div>
|
|
265
|
-
<span className="
|
|
273
|
+
<span className="sessions-card-chevron">›</span>
|
|
266
274
|
</div>
|
|
267
275
|
))}
|
|
268
276
|
|