● Zion Boggan
repos/Darwin/face/src/components/workspace/TermPane.tsx
zionboggan.com ↗
271 lines · tsx
History for this file →
1
"use client";
2
 
3
import { useEffect } from "react";
4
import { useTermSocket } from "@/hooks/useTermSocket";
5
import type { TermCreds, LaunchSpec, AgentKind } from "@/lib/term";
6
import { AGENT_LABEL } from "@/lib/term";
7
 
8
export interface PaneState {
9
  id: string;
10
  spec: LaunchSpec;
11
}
12
 
13
interface TermPaneProps {
14
  pane: PaneState;
15
  creds: TermCreds | null;
16
  visible: boolean;
17
  focused: boolean;
18
  onFocus: () => void;
19
  onClose: () => void;
20
  onAssign?: (paneId: string) => void;
21
  registerFit?: (paneId: string, fitFn: (() => void) | null) => void;
22
  index: number;
23
}
24
 
25
const STATUS_META: Record<string, { label: string; color: string; pulse: boolean }> = {
26
  idle: { label: "IDLE", color: "#4a525e", pulse: false },
27
  connecting: { label: "CONNECTING", color: "#e8c468", pulse: true },
28
  live: { label: "LIVE", color: "#16e0c8", pulse: true },
29
  closed: { label: "CLOSED", color: "#4a525e", pulse: false },
30
  error: { label: "ERROR", color: "#ff6b6b", pulse: false },
31
};
32
 
33
export default function TermPane({
34
  pane,
35
  creds,
36
  visible,
37
  focused,
38
  onFocus,
39
  onClose,
40
  onAssign,
41
  registerFit,
42
  index,
43
}: TermPaneProps) {
44
  const { containerRef, status, sessionName, fit, fontSize, setFontSize } = useTermSocket({
45
    creds,
46
    spec: pane.spec,
47
    visible,
48
  });
49
 
50
  useEffect(() => {
51
    if (!registerFit) return;
52
    registerFit(pane.id, fit);
53
    return () => {
54
      registerFit(pane.id, null);
55
    };
56
  }, [pane.id, fit, registerFit]);
57
 
58
  const agent = pane.spec.agent as AgentKind;
59
  const meta = STATUS_META[status] ?? STATUS_META.idle;
60
 
61
  return (
62
    <div
63
      onMouseDown={onFocus}
64
      className="flex h-full w-full flex-col overflow-hidden"
65
      style={{
66
        background: "#050607",
67
        border: focused
68
          ? "1px solid rgba(22,224,200,0.45)"
69
          : "1px solid rgba(22,224,200,0.10)",
70
        boxShadow: focused ? "0 0 0 1px rgba(22,224,200,0.10), inset 0 0 80px rgba(22,224,200,0.02)" : "none",
71
        transition: "border-color 0.3s cubic-bezier(0.16,1,0.3,1)",
72
      }}
73
    >
74
      {}
75
      <div
76
        className="flex shrink-0 items-center justify-between gap-2 px-3"
77
        style={{
78
          height: 34,
79
          background: "rgba(10,12,14,0.92)",
80
          borderBottom: "1px solid rgba(22,224,200,0.10)",
81
        }}
82
      >
83
        <div className="flex min-w-0 items-center gap-2.5">
84
          <span
85
            className="font-mono"
86
            style={{ fontSize: "0.6rem", letterSpacing: "0.12em", color: "#4a525e" }}
87
          >
88
            {String(index + 1).padStart(2, "0")}
89
          </span>
90
          <span
91
            className="font-display font-bold uppercase"
92
            style={{ fontSize: "0.72rem", letterSpacing: "0.06em", color: "#e8eaed" }}
93
          >
94
            {AGENT_LABEL[agent]}
95
          </span>
96
          <span
97
            className="font-mono truncate"
98
            style={{ fontSize: "0.62rem", letterSpacing: "0.04em", color: "#4a525e" }}
99
          >
100
            {pane.spec.target}
101
            {pane.spec.cwd ? ` · ${pane.spec.cwd.split("/").pop() || pane.spec.cwd}` : ""}
102
          </span>
103
        </div>
104
 
105
        <div className="flex shrink-0 items-center gap-3">
106
          {sessionName && (
107
            <span
108
              className="font-mono hidden sm:inline truncate"
109
              style={{ fontSize: "0.56rem", letterSpacing: "0.06em", color: "#0d9e8e", maxWidth: 130 }}
110
              title={`tmux session - reattach: ${sessionName}`}
111
            >
112
              ⟲ {sessionName}
113
            </span>
114
          )}
115
          {onAssign && (
116
            <button
117
              onClick={(e) => {
118
                e.stopPropagation();
119
                onAssign(pane.id);
120
              }}
121
              title="Assign an existing session to this pane"
122
              className="font-mono uppercase"
123
              style={{
124
                fontSize: "0.56rem",
125
                letterSpacing: "0.08em",
126
                color: "#16e0c8",
127
                padding: "2px 5px",
128
                border: "1px solid rgba(22,224,200,0.24)",
129
                transition: "background 0.2s",
130
              }}
131
              onMouseEnter={(e) => (e.currentTarget.style.background = "rgba(22,224,200,0.10)")}
132
              onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
133
            >
134
              ⇄ ASSIGN
135
            </button>
136
          )}
137
          {}
138
          <span
139
            className="flex shrink-0 items-center font-mono"
140
            style={{ border: "1px solid rgba(22,224,200,0.24)" }}
141
          >
142
            <button
143
              type="button"
144
              onClick={(e) => {
145
                e.stopPropagation();
146
                setFontSize(fontSize - 1);
147
              }}
148
              onMouseDown={(e) => e.stopPropagation()}
149
              title="Smaller font"
150
              className="flex items-center justify-center"
151
              style={{
152
                height: 22,
153
                minWidth: 22,
154
                padding: "0 4px",
155
                fontSize: "0.56rem",
156
                lineHeight: 1,
157
                color: fontSize <= 9 ? "#4a525e" : "#8b929e",
158
                background: "transparent",
159
                transition: "background 0.2s, color 0.2s",
160
              }}
161
              onMouseEnter={(e) => {
162
                e.currentTarget.style.background = "rgba(22,224,200,0.10)";
163
                e.currentTarget.style.color = "#16e0c8";
164
              }}
165
              onMouseLeave={(e) => {
166
                e.currentTarget.style.background = "transparent";
167
                e.currentTarget.style.color = fontSize <= 9 ? "#4a525e" : "#8b929e";
168
              }}
169
            >
170
              A−
171
            </button>
172
            <span
173
              title="Terminal font size (px)"
174
              className="flex items-center justify-center"
175
              style={{
176
                height: 22,
177
                minWidth: 16,
178
                padding: "0 5px",
179
                fontSize: "0.56rem",
180
                letterSpacing: "0.02em",
181
                lineHeight: 1,
182
                color: "#16e0c8",
183
                borderLeft: "1px solid rgba(22,224,200,0.18)",
184
                borderRight: "1px solid rgba(22,224,200,0.18)",
185
              }}
186
            >
187
              {fontSize}
188
            </span>
189
            <button
190
              type="button"
191
              onClick={(e) => {
192
                e.stopPropagation();
193
                setFontSize(fontSize + 1);
194
              }}
195
              onMouseDown={(e) => e.stopPropagation()}
196
              title="Larger font"
197
              className="flex items-center justify-center"
198
              style={{
199
                height: 22,
200
                minWidth: 22,
201
                padding: "0 4px",
202
                fontSize: "0.56rem",
203
                lineHeight: 1,
204
                color: fontSize >= 20 ? "#4a525e" : "#8b929e",
205
                background: "transparent",
206
                transition: "background 0.2s, color 0.2s",
207
              }}
208
              onMouseEnter={(e) => {
209
                e.currentTarget.style.background = "rgba(22,224,200,0.10)";
210
                e.currentTarget.style.color = "#16e0c8";
211
              }}
212
              onMouseLeave={(e) => {
213
                e.currentTarget.style.background = "transparent";
214
                e.currentTarget.style.color = fontSize >= 20 ? "#4a525e" : "#8b929e";
215
              }}
216
            >
217
              A+
218
            </button>
219
          </span>
220
          <span className="flex items-center gap-1.5">
221
            <span
222
              className="rounded-full"
223
              style={{
224
                width: 6,
225
                height: 6,
226
                background: meta.color,
227
                boxShadow: meta.pulse ? `0 0 8px ${meta.color}` : "none",
228
                animation: meta.pulse ? "glow-breathe 2.4s ease-in-out infinite" : "none",
229
              }}
230
            />
231
            <span
232
              className="font-mono"
233
              style={{ fontSize: "0.54rem", letterSpacing: "0.10em", color: meta.color }}
234
            >
235
              {meta.label}
236
            </span>
237
          </span>
238
          <button
239
            onClick={(e) => {
240
              e.stopPropagation();
241
              onClose();
242
            }}
243
            aria-label="Close pane"
244
            className="flex items-center justify-center"
245
            style={{
246
              width: 22,
247
              height: 22,
248
              color: "#4a525e",
249
              transition: "color 0.2s",
250
            }}
251
            onMouseEnter={(e) => (e.currentTarget.style.color = "#ff6b6b")}
252
            onMouseLeave={(e) => (e.currentTarget.style.color = "#4a525e")}
253
          >
254
            <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
255
              <path d="M1 1L10 10M10 1L1 10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
256
            </svg>
257
          </button>
258
        </div>
259
      </div>
260
 
261
      {}
262
      <div className="relative min-h-0 flex-1">
263
        <div
264
          ref={containerRef}
265
          className="absolute inset-0"
266
          style={{ padding: "2px 4px", overflow: "hidden" }}
267
        />
268
      </div>
269
    </div>
270
  );
271
}