palmier 0.7.7 → 0.7.9
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/agents/agent.d.ts +3 -0
- package/dist/agents/agent.js +1 -1
- package/dist/agents/aider.d.ts +1 -0
- package/dist/agents/aider.js +1 -0
- package/dist/agents/claude.d.ts +1 -0
- package/dist/agents/claude.js +1 -0
- package/dist/agents/cline.d.ts +1 -0
- package/dist/agents/cline.js +1 -0
- package/dist/agents/codex.d.ts +1 -0
- package/dist/agents/codex.js +1 -0
- package/dist/agents/copilot.d.ts +1 -0
- package/dist/agents/copilot.js +1 -0
- package/dist/agents/cursor.d.ts +1 -0
- package/dist/agents/cursor.js +1 -0
- package/dist/agents/deepagents.d.ts +1 -0
- package/dist/agents/deepagents.js +1 -0
- package/dist/agents/droid.d.ts +1 -0
- package/dist/agents/droid.js +1 -0
- package/dist/agents/gemini.d.ts +1 -0
- package/dist/agents/gemini.js +1 -0
- package/dist/agents/goose.d.ts +1 -0
- package/dist/agents/goose.js +1 -0
- package/dist/agents/hermes.d.ts +1 -0
- package/dist/agents/hermes.js +1 -0
- package/dist/agents/kimi.d.ts +1 -0
- package/dist/agents/kimi.js +1 -0
- package/dist/agents/kiro.d.ts +1 -0
- package/dist/agents/kiro.js +1 -0
- package/dist/agents/openclaw.d.ts +1 -0
- package/dist/agents/openclaw.js +2 -2
- package/dist/agents/opencode.d.ts +1 -0
- package/dist/agents/opencode.js +1 -0
- package/dist/agents/qoder.d.ts +1 -0
- package/dist/agents/qoder.js +1 -0
- package/dist/agents/qwen.d.ts +1 -0
- package/dist/agents/qwen.js +1 -0
- package/dist/commands/pair.js +2 -2
- package/dist/mcp-tools.d.ts +2 -0
- package/dist/mcp-tools.js +20 -9
- package/dist/pending-requests.d.ts +30 -8
- package/dist/pending-requests.js +28 -15
- package/dist/platform/linux.js +11 -8
- package/dist/platform/windows.d.ts +5 -6
- package/dist/platform/windows.js +15 -12
- package/dist/pwa/assets/index-FP1Mipr6.js +120 -0
- package/dist/pwa/assets/index-bLTn8zBj.css +1 -0
- package/dist/pwa/assets/{web-CkWrlNwc.js → web-BpM3fNCn.js} +1 -1
- package/dist/pwa/assets/{web-lx34oBi7.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 +35 -24
- package/dist/task.js +1 -1
- package/dist/transports/http-transport.js +9 -8
- package/dist/types.d.ts +11 -6
- package/package.json +1 -1
- package/palmier-server/README.md +3 -3
- package/palmier-server/pwa/src/App.css +175 -28
- package/palmier-server/pwa/src/App.tsx +1 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +7 -2
- 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 +147 -0
- package/palmier-server/pwa/src/components/{RunsView.tsx → SessionsView.tsx} +79 -45
- package/palmier-server/pwa/src/components/TabBar.tsx +17 -10
- package/palmier-server/pwa/src/components/TaskCard.tsx +33 -35
- package/palmier-server/pwa/src/components/TaskForm.tsx +275 -349
- package/palmier-server/pwa/src/components/TasksView.tsx +172 -0
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +16 -8
- package/palmier-server/pwa/src/draftGuard.ts +24 -0
- package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +102 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +343 -37
- package/palmier-server/pwa/src/types.ts +5 -14
- package/palmier-server/spec.md +39 -26
- package/src/agents/agent.ts +5 -1
- package/src/agents/aider.ts +1 -0
- package/src/agents/claude.ts +1 -0
- package/src/agents/cline.ts +1 -0
- package/src/agents/codex.ts +1 -0
- package/src/agents/copilot.ts +1 -0
- package/src/agents/cursor.ts +1 -0
- package/src/agents/deepagents.ts +1 -0
- package/src/agents/droid.ts +1 -0
- package/src/agents/gemini.ts +1 -0
- package/src/agents/goose.ts +1 -0
- package/src/agents/hermes.ts +1 -0
- package/src/agents/kimi.ts +1 -0
- package/src/agents/kiro.ts +1 -0
- package/src/agents/openclaw.ts +2 -2
- package/src/agents/opencode.ts +1 -0
- package/src/agents/qoder.ts +1 -0
- package/src/agents/qwen.ts +1 -0
- package/src/commands/pair.ts +2 -2
- package/src/mcp-tools.ts +22 -9
- package/src/pending-requests.ts +47 -15
- package/src/platform/linux.ts +10 -8
- package/src/platform/windows.ts +15 -12
- package/src/rpc-handler.ts +39 -26
- package/src/task.ts +1 -1
- package/src/transports/http-transport.ts +9 -8
- package/src/types.ts +10 -8
- package/test/pairing.test.ts +2 -2
- package/dist/pwa/assets/index-B-ByUHPS.css +0 -1
- package/dist/pwa/assets/index-Bt8Hhaw3.js +0 -118
- package/palmier-server/pwa/src/components/TaskListView.tsx +0 -431
|
@@ -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 {
|
|
@@ -578,26 +606,104 @@ body {
|
|
|
578
606
|
}
|
|
579
607
|
}
|
|
580
608
|
|
|
581
|
-
/* =====
|
|
582
|
-
.
|
|
609
|
+
/* ===== Session Composer ===== */
|
|
610
|
+
.session-composer {
|
|
583
611
|
background: var(--color-surface);
|
|
584
612
|
border-radius: var(--radius-md);
|
|
585
613
|
border: 1px solid var(--color-border);
|
|
586
614
|
box-shadow: var(--shadow-sm);
|
|
587
|
-
padding: var(--space-
|
|
588
|
-
margin-bottom: var(--space-
|
|
589
|
-
|
|
615
|
+
padding: var(--space-sm);
|
|
616
|
+
margin-bottom: var(--space-md);
|
|
617
|
+
display: flex;
|
|
618
|
+
flex-direction: column;
|
|
619
|
+
gap: var(--space-sm);
|
|
590
620
|
}
|
|
591
621
|
|
|
592
|
-
.
|
|
622
|
+
.session-composer:focus-within {
|
|
593
623
|
border-color: var(--color-primary);
|
|
624
|
+
box-shadow: 0 0 0 2px var(--color-input-focus);
|
|
594
625
|
}
|
|
595
626
|
|
|
596
|
-
.
|
|
597
|
-
|
|
627
|
+
.session-composer-textarea {
|
|
628
|
+
width: 100%;
|
|
629
|
+
border: none;
|
|
630
|
+
resize: vertical;
|
|
631
|
+
outline: none;
|
|
632
|
+
background: transparent;
|
|
633
|
+
color: var(--color-text);
|
|
634
|
+
font-family: var(--font-sans);
|
|
635
|
+
font-size: 0.9375rem;
|
|
636
|
+
line-height: 1.5;
|
|
637
|
+
padding: var(--space-xs) var(--space-sm);
|
|
638
|
+
min-height: 3.5em;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.session-composer-textarea::placeholder {
|
|
598
642
|
color: var(--color-muted);
|
|
599
643
|
}
|
|
600
644
|
|
|
645
|
+
.session-composer-controls {
|
|
646
|
+
display: flex;
|
|
647
|
+
align-items: center;
|
|
648
|
+
gap: var(--space-sm);
|
|
649
|
+
padding-left: var(--space-xs);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.session-composer-yolo {
|
|
653
|
+
display: inline-flex;
|
|
654
|
+
align-items: center;
|
|
655
|
+
gap: 4px;
|
|
656
|
+
font-size: 0.8rem;
|
|
657
|
+
color: var(--color-text-secondary);
|
|
658
|
+
cursor: pointer;
|
|
659
|
+
user-select: none;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.session-composer-yolo input {
|
|
663
|
+
margin: 0;
|
|
664
|
+
cursor: pointer;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.session-composer-controls .chat-send-btn {
|
|
668
|
+
margin-left: auto;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/* ===== Task List ===== */
|
|
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;
|
|
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;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.fab:hover {
|
|
693
|
+
background: var(--color-primary-hover);
|
|
694
|
+
box-shadow: var(--shadow-xl);
|
|
695
|
+
transform: translateY(-1px);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.fab:active {
|
|
699
|
+
transform: translateY(0);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.fab:focus-visible {
|
|
703
|
+
outline: 2px solid var(--color-primary);
|
|
704
|
+
outline-offset: 3px;
|
|
705
|
+
}
|
|
706
|
+
|
|
601
707
|
.section-label {
|
|
602
708
|
font-size: 0.6875rem;
|
|
603
709
|
font-weight: 600;
|
|
@@ -611,7 +717,6 @@ body {
|
|
|
611
717
|
display: flex;
|
|
612
718
|
align-items: center;
|
|
613
719
|
gap: var(--space-xs);
|
|
614
|
-
margin-left: auto;
|
|
615
720
|
}
|
|
616
721
|
|
|
617
722
|
.agent-picker-section-inline .agent-picker-label {
|
|
@@ -1096,15 +1201,57 @@ body {
|
|
|
1096
1201
|
font-weight: 600;
|
|
1097
1202
|
}
|
|
1098
1203
|
|
|
1099
|
-
.
|
|
1204
|
+
.granted-permissions-row {
|
|
1205
|
+
flex-basis: 100%;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.granted-permissions-row .btn-link {
|
|
1209
|
+
padding: 0;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
.schedule-section {
|
|
1100
1213
|
display: flex;
|
|
1101
1214
|
flex-direction: column;
|
|
1102
|
-
gap:
|
|
1215
|
+
gap: var(--space-sm);
|
|
1103
1216
|
}
|
|
1104
1217
|
|
|
1105
|
-
.
|
|
1106
|
-
|
|
1107
|
-
|
|
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;
|
|
1108
1255
|
}
|
|
1109
1256
|
|
|
1110
1257
|
.trigger-row-card {
|
|
@@ -1152,7 +1299,7 @@ body {
|
|
|
1152
1299
|
width: auto;
|
|
1153
1300
|
}
|
|
1154
1301
|
|
|
1155
|
-
.
|
|
1302
|
+
.schedule-section > .form-select,
|
|
1156
1303
|
.trigger-row-card .form-select,
|
|
1157
1304
|
.trigger-row-card .form-input {
|
|
1158
1305
|
margin-bottom: 0;
|
|
@@ -1163,7 +1310,7 @@ body {
|
|
|
1163
1310
|
min-width: 0;
|
|
1164
1311
|
}
|
|
1165
1312
|
|
|
1166
|
-
.
|
|
1313
|
+
.schedule-section > .form-select {
|
|
1167
1314
|
width: 100%;
|
|
1168
1315
|
}
|
|
1169
1316
|
|
|
@@ -1855,9 +2002,9 @@ body {
|
|
|
1855
2002
|
border-bottom-color: var(--color-primary);
|
|
1856
2003
|
}
|
|
1857
2004
|
|
|
1858
|
-
/* =====
|
|
2005
|
+
/* ===== Sessions view ===== */
|
|
1859
2006
|
|
|
1860
|
-
.
|
|
2007
|
+
.sessions-view {
|
|
1861
2008
|
flex: 1;
|
|
1862
2009
|
display: flex;
|
|
1863
2010
|
flex-direction: column;
|
|
@@ -1865,7 +2012,7 @@ body {
|
|
|
1865
2012
|
justify-content: center;
|
|
1866
2013
|
}
|
|
1867
2014
|
|
|
1868
|
-
.
|
|
2015
|
+
.sessions-card {
|
|
1869
2016
|
background: var(--color-surface);
|
|
1870
2017
|
border-radius: var(--radius-md);
|
|
1871
2018
|
border: 1px solid var(--color-border);
|
|
@@ -1879,12 +2026,12 @@ body {
|
|
|
1879
2026
|
-webkit-tap-highlight-color: transparent;
|
|
1880
2027
|
}
|
|
1881
2028
|
|
|
1882
|
-
.
|
|
2029
|
+
.sessions-card:hover {
|
|
1883
2030
|
box-shadow: var(--shadow-md);
|
|
1884
2031
|
border-color: #CBD5E1;
|
|
1885
2032
|
}
|
|
1886
2033
|
|
|
1887
|
-
.
|
|
2034
|
+
.sessions-card-body {
|
|
1888
2035
|
flex: 1;
|
|
1889
2036
|
min-width: 0;
|
|
1890
2037
|
display: flex;
|
|
@@ -1892,7 +2039,7 @@ body {
|
|
|
1892
2039
|
gap: var(--space-xs);
|
|
1893
2040
|
}
|
|
1894
2041
|
|
|
1895
|
-
.
|
|
2042
|
+
.sessions-card-name {
|
|
1896
2043
|
font-size: 0.9375rem;
|
|
1897
2044
|
font-weight: 600;
|
|
1898
2045
|
letter-spacing: -0.01em;
|
|
@@ -1902,7 +2049,7 @@ body {
|
|
|
1902
2049
|
text-overflow: ellipsis;
|
|
1903
2050
|
}
|
|
1904
2051
|
|
|
1905
|
-
.
|
|
2052
|
+
.sessions-card-meta {
|
|
1906
2053
|
display: flex;
|
|
1907
2054
|
flex-wrap: wrap;
|
|
1908
2055
|
gap: var(--space-sm);
|
|
@@ -1910,7 +2057,7 @@ body {
|
|
|
1910
2057
|
color: var(--color-text-secondary);
|
|
1911
2058
|
}
|
|
1912
2059
|
|
|
1913
|
-
.
|
|
2060
|
+
.sessions-card-chevron {
|
|
1914
2061
|
flex-shrink: 0;
|
|
1915
2062
|
align-self: center;
|
|
1916
2063
|
color: var(--color-text-secondary);
|
|
@@ -1920,11 +2067,11 @@ body {
|
|
|
1920
2067
|
transition: opacity var(--transition-base);
|
|
1921
2068
|
}
|
|
1922
2069
|
|
|
1923
|
-
.
|
|
2070
|
+
.sessions-card:hover .sessions-card-chevron {
|
|
1924
2071
|
opacity: 0.8;
|
|
1925
2072
|
}
|
|
1926
2073
|
|
|
1927
|
-
.
|
|
2074
|
+
.sessions-filter-chip {
|
|
1928
2075
|
display: inline-flex;
|
|
1929
2076
|
align-items: center;
|
|
1930
2077
|
gap: var(--space-xs);
|
|
@@ -1936,7 +2083,7 @@ body {
|
|
|
1936
2083
|
border-radius: 999px;
|
|
1937
2084
|
}
|
|
1938
2085
|
|
|
1939
|
-
.
|
|
2086
|
+
.sessions-filter-chip button {
|
|
1940
2087
|
display: inline-flex;
|
|
1941
2088
|
align-items: center;
|
|
1942
2089
|
justify-content: center;
|
|
@@ -1949,7 +2096,7 @@ body {
|
|
|
1949
2096
|
line-height: 1;
|
|
1950
2097
|
}
|
|
1951
2098
|
|
|
1952
|
-
.
|
|
2099
|
+
.sessions-filter-chip button:hover {
|
|
1953
2100
|
color: var(--color-text);
|
|
1954
2101
|
}
|
|
1955
2102
|
|
|
@@ -10,6 +10,7 @@ export default function App() {
|
|
|
10
10
|
<HostConnectionProvider>
|
|
11
11
|
<Routes>
|
|
12
12
|
<Route path="/" element={<Dashboard />} />
|
|
13
|
+
<Route path="/tasks" element={<Dashboard />} />
|
|
13
14
|
<Route path="/runs" element={<Dashboard />} />
|
|
14
15
|
<Route path="/runs/:taskId" element={<Dashboard />} />
|
|
15
16
|
<Route path="/runs/:taskId/:runId" element={<Dashboard />} />
|
|
@@ -98,6 +98,7 @@ const FullScreenIntent = Capacitor.isNativePlatform()
|
|
|
98
98
|
: null;
|
|
99
99
|
import { useHostStore } from "../contexts/HostStoreContext";
|
|
100
100
|
import { useMediaQuery } from "../hooks/useMediaQuery";
|
|
101
|
+
import { confirmLeaveDraft } from "../draftGuard";
|
|
101
102
|
|
|
102
103
|
/** LAN mode: PWA is served by palmier serve (marker injected into HTML). */
|
|
103
104
|
const isLanMode = !!(window as any).__PALMIER_SERVE__;
|
|
@@ -500,7 +501,11 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
500
501
|
className={`host-picker-item ${isActive ? "host-picker-item-active" : ""}`}
|
|
501
502
|
onClick={() => {
|
|
502
503
|
if (isRenaming) return;
|
|
503
|
-
if (!isActive) {
|
|
504
|
+
if (!isActive) {
|
|
505
|
+
if (!confirmLeaveDraft()) return;
|
|
506
|
+
setActiveHostId(host.hostId);
|
|
507
|
+
if (!isDesktop) close();
|
|
508
|
+
}
|
|
504
509
|
}}
|
|
505
510
|
role="option"
|
|
506
511
|
aria-selected={isActive}
|
|
@@ -580,7 +585,7 @@ export default function HostMenu({ daemonVersion, capabilityTokens, activeClient
|
|
|
580
585
|
<div className="drawer-section">
|
|
581
586
|
<button
|
|
582
587
|
className="btn btn-primary btn-full"
|
|
583
|
-
onClick={() => { navigate("/pair"); if (!isDesktop) close(); }}
|
|
588
|
+
onClick={() => { if (!confirmLeaveDraft()) return; navigate("/pair"); if (!isDesktop) close(); }}
|
|
584
589
|
>
|
|
585
590
|
Pair New Host
|
|
586
591
|
</button>
|
|
@@ -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"
|