● Zion Boggan
repos/Darwin/face/src/components/workspace/SessionPicker.tsx
zionboggan.com ↗
244 lines · tsx
History for this file →
1
"use client";
2
 
3
import { useEffect, useState } from "react";
4
import type { LiveSession, AgentKind } from "@/lib/term";
5
import { AGENT_LABEL } from "@/lib/term";
6
 
7
interface SessionPickerProps {
8
  openSessionIds: Set<string>;
9
  onPick: (s: LiveSession) => void;
10
  onClose: () => void;
11
  onLaunchNew: () => void;
12
}
13
 
14
function ageOf(created: string): string {
15
  const t = parseInt(created, 10);
16
  if (!t) return "";
17
  const s = Math.max(0, Math.floor(Date.now() / 1000 - t));
18
  if (s < 60) return `${s}s`;
19
  if (s < 3600) return `${Math.floor(s / 60)}m`;
20
  if (s < 86400) return `${Math.floor(s / 3600)}h`;
21
  return `${Math.floor(s / 86400)}d`;
22
}
23
 
24
function truncate(s: string, n: number): string {
25
  return s.length > n ? `${s.slice(0, n)}…` : s;
26
}
27
 
28
export default function SessionPicker({
29
  openSessionIds,
30
  onPick,
31
  onClose,
32
  onLaunchNew,
33
}: SessionPickerProps) {
34
  const [sessions, setSessions] = useState<LiveSession[]>([]);
35
 
36
  useEffect(() => {
37
    let alive = true;
38
    const tick = async () => {
39
      try {
40
        const res = await fetch("/api/term/sessions", { cache: "no-store" });
41
        const data = await res.json();
42
        if (alive) setSessions((data.sessions ?? []) as LiveSession[]);
43
      } catch {
44
      }
45
    };
46
    tick();
47
    const id = setInterval(tick, 5000);
48
    return () => {
49
      alive = false;
50
      clearInterval(id);
51
    };
52
  }, []);
53
 
54
  useEffect(() => {
55
    const onKey = (e: KeyboardEvent) => {
56
      if (e.key === "Escape") onClose();
57
    };
58
    window.addEventListener("keydown", onKey);
59
    return () => window.removeEventListener("keydown", onKey);
60
  }, [onClose]);
61
 
62
  const sorted = [...sessions].sort(
63
    (a, b) => parseInt(b.created, 10) - parseInt(a.created, 10)
64
  );
65
 
66
  return (
67
    <div
68
      className="fixed inset-0 z-[60] flex items-center justify-center"
69
      style={{ background: "rgba(5,6,7,0.6)", backdropFilter: "blur(2px)" }}
70
      onClick={onClose}
71
    >
72
      <div
73
        onClick={(e) => e.stopPropagation()}
74
        className="flex flex-col"
75
        style={{
76
          background: "#080a0c",
77
          border: "1px solid rgba(22,224,200,0.2)",
78
          maxWidth: 520,
79
          width: "calc(100% - 32px)",
80
          maxHeight: "70vh",
81
        }}
82
      >
83
        {}
84
        <div
85
          className="flex shrink-0 items-center justify-between"
86
          style={{ padding: "12px 14px", borderBottom: "1px solid rgba(22,224,200,0.12)" }}
87
        >
88
          <div className="flex items-center gap-2 min-w-0">
89
            <span
90
              className="font-mono uppercase shrink-0"
91
              style={{ fontSize: "0.55rem", letterSpacing: "0.2em", color: "#0d9e8e" }}
92
            >
93
              ⇄ Assign
94
            </span>
95
            <span
96
              className="font-display font-bold uppercase truncate"
97
              style={{ fontSize: "0.82rem", letterSpacing: "0.04em", color: "#e8eaed" }}
98
            >
99
              Live Sessions
100
            </span>
101
          </div>
102
          <button
103
            onClick={onClose}
104
            aria-label="Close"
105
            className="flex shrink-0 items-center justify-center"
106
            style={{ width: 24, height: 24, color: "#4a525e", transition: "color 0.2s" }}
107
            onMouseEnter={(e) => (e.currentTarget.style.color = "#ff6b6b")}
108
            onMouseLeave={(e) => (e.currentTarget.style.color = "#4a525e")}
109
          >
110
            <svg width="12" height="12" viewBox="0 0 11 11" fill="none">
111
              <path d="M1 1L10 10M10 1L1 10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
112
            </svg>
113
          </button>
114
        </div>
115
 
116
        {}
117
        <div className="min-h-0 flex-1 overflow-y-auto">
118
          {sorted.length === 0 && (
119
            <p
120
              className="font-mono"
121
              style={{
122
                fontSize: "0.62rem",
123
                color: "#4a525e",
124
                letterSpacing: "0.03em",
125
                padding: "24px 14px",
126
                lineHeight: 1.6,
127
              }}
128
            >
129
              No sessions running. Launch a new one below.
130
            </p>
131
          )}
132
 
133
          {sorted.map((s) => {
134
            const inPane = openSessionIds.has(s.session);
135
            const agent = (s.agent as AgentKind) || "shell";
136
            return (
137
              <button
138
                key={s.session}
139
                onClick={() => onPick(s)}
140
                className="flex w-full items-center gap-2.5 text-left"
141
                style={{
142
                  padding: "10px 14px",
143
                  borderBottom: "1px solid rgba(22,224,200,0.06)",
144
                  transition: "background 0.15s",
145
                }}
146
                onMouseEnter={(e) => (e.currentTarget.style.background = "rgba(22,224,200,0.06)")}
147
                onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
148
              >
149
                {}
150
                <span className="flex shrink-0 items-center justify-center" style={{ width: 6, height: 6 }}>
151
                  {inPane && (
152
                    <span
153
                      style={{
154
                        width: 6,
155
                        height: 6,
156
                        background: "#f5c518",
157
                        boxShadow: "0 0 6px #f5c518",
158
                        display: "block",
159
                      }}
160
                    />
161
                  )}
162
                </span>
163
 
164
                <div className="min-w-0 flex-1">
165
                  <div className="flex items-center gap-2">
166
                    <span
167
                      className="font-display font-bold uppercase"
168
                      style={{ fontSize: "0.72rem", color: "#e8eaed", letterSpacing: "0.03em" }}
169
                    >
170
                      {AGENT_LABEL[agent] ?? s.agent}
171
                    </span>
172
                    <span className="font-mono" style={{ fontSize: "0.58rem", color: "#8b929e", letterSpacing: "0.04em" }}>
173
                      {s.target}
174
                    </span>
175
                    {inPane && (
176
                      <span className="font-mono" style={{ fontSize: "0.5rem", color: "#f5c518", letterSpacing: "0.08em" }}>
177
                        IN PANE
178
                      </span>
179
                    )}
180
                    {s.attached && (
181
                      <span
182
                        className="font-mono"
183
                        style={{
184
                          fontSize: "0.5rem",
185
                          color: "#0d9e8e",
186
                          letterSpacing: "0.06em",
187
                          border: "1px solid rgba(22,224,200,0.24)",
188
                          padding: "1px 4px",
189
                        }}
190
                      >
191
                        OPEN
192
                      </span>
193
                    )}
194
                  </div>
195
                  <div className="flex items-center gap-2">
196
                    <span
197
                      className="font-mono truncate"
198
                      style={{ fontSize: "0.54rem", color: "#4a525e", letterSpacing: "0.04em", maxWidth: 320 }}
199
                      title={s.session}
200
                    >
201
                      {truncate(s.session, 26)}
202
                    </span>
203
                    <span className="font-mono shrink-0" style={{ fontSize: "0.52rem", color: "#4a525e", letterSpacing: "0.04em" }}>
204
                      {ageOf(s.created)} ago
205
                    </span>
206
                  </div>
207
                </div>
208
 
209
                <span
210
                  className="font-display font-bold uppercase shrink-0"
211
                  style={{ fontSize: "0.6rem", color: "#16e0c8", letterSpacing: "0.06em" }}
212
                >
213
                  Load ▸
214
                </span>
215
              </button>
216
            );
217
          })}
218
        </div>
219
 
220
        {}
221
        <div className="shrink-0" style={{ padding: "10px 14px", borderTop: "1px solid rgba(22,224,200,0.12)" }}>
222
          <button
223
            onClick={onLaunchNew}
224
            className="font-display font-bold uppercase w-full"
225
            style={{
226
              height: 40,
227
              fontSize: "0.72rem",
228
              letterSpacing: "0.08em",
229
              color: "#050607",
230
              background: "#16e0c8",
231
              border: "none",
232
              cursor: "pointer",
233
              transition: "filter 0.2s",
234
            }}
235
            onMouseEnter={(e) => (e.currentTarget.style.filter = "brightness(1.12)")}
236
            onMouseLeave={(e) => (e.currentTarget.style.filter = "brightness(1)")}
237
          >
238
            + Launch New
239
          </button>
240
        </div>
241
      </div>
242
    </div>
243
  );
244
}