feat(status-ui): enhance desktop session controls
Add native directory selection, persisted interface scaling, and navigation back to the latest conversation message. Restore saved session transcripts when the bridge transcript RPC is unavailable so the desktop client remains useful during bridge compatibility gaps.
This commit is contained in:
+125
-37
@@ -6,6 +6,7 @@
|
||||
|
||||
:root {
|
||||
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 18px;
|
||||
color: #ebebe8;
|
||||
background: #101111;
|
||||
}
|
||||
@@ -27,8 +28,8 @@
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 480px;
|
||||
min-height: 420px;
|
||||
min-width: 640px;
|
||||
min-height: 520px;
|
||||
background: transparent;
|
||||
}
|
||||
button,
|
||||
@@ -41,10 +42,10 @@ button {
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
padding: 8px 12px;
|
||||
padding: 9px 14px;
|
||||
background: #dcecc8;
|
||||
color: #152012;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 750;
|
||||
}
|
||||
button:hover {
|
||||
@@ -66,8 +67,8 @@ textarea:focus-visible {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 9px;
|
||||
padding: 14px;
|
||||
gap: 12px;
|
||||
padding: 18px;
|
||||
border: 1px solid #3b403a;
|
||||
border-radius: 14px;
|
||||
background: rgba(18, 20, 19, 0.97);
|
||||
@@ -101,23 +102,23 @@ textarea:focus-visible {
|
||||
.brand {
|
||||
flex: 0 0 auto;
|
||||
color: #b8df84;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
h1 {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-size: 20px;
|
||||
line-height: 1.15;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-row {
|
||||
margin-top: 3px;
|
||||
margin-top: 4px;
|
||||
color: #898f89;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -139,10 +140,10 @@ h1 {
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid #404740;
|
||||
border-radius: 999px;
|
||||
padding: 2px 6px;
|
||||
padding: 3px 8px;
|
||||
color: #afb3ad;
|
||||
background: #202320;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.state-pill.working {
|
||||
@@ -171,8 +172,8 @@ h1 {
|
||||
.directory-tab-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 32px;
|
||||
gap: 7px;
|
||||
min-height: 38px;
|
||||
}
|
||||
.directory-tabs {
|
||||
overflow-x: auto;
|
||||
@@ -184,7 +185,7 @@ h1 {
|
||||
.agent-caption {
|
||||
flex: 0 0 auto;
|
||||
color: #70776f;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
@@ -197,16 +198,16 @@ h1 {
|
||||
.directory-tab-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
max-width: 180px;
|
||||
max-width: 220px;
|
||||
border: 1px solid #303530;
|
||||
border-right: 0;
|
||||
border-radius: 7px 0 0 7px;
|
||||
padding: 5px 8px;
|
||||
padding: 7px 10px;
|
||||
background: #1b1e1b;
|
||||
color: #aeb4ac;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
}
|
||||
.directory-tab-button > span {
|
||||
@@ -216,7 +217,7 @@ h1 {
|
||||
}
|
||||
.directory-tab-button small {
|
||||
color: #70776f;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.directory-tab-button.selected {
|
||||
@@ -236,13 +237,13 @@ h1 {
|
||||
border-color: #7da856;
|
||||
}
|
||||
.directory-close {
|
||||
min-width: 24px;
|
||||
min-width: 30px;
|
||||
border: 1px solid #303530;
|
||||
border-radius: 0 7px 7px 0;
|
||||
padding: 3px 6px;
|
||||
padding: 4px 7px;
|
||||
background: #1b1e1b;
|
||||
color: #aeb4ac;
|
||||
font-size: 15px;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
.directory-tab:focus-within .directory-tab-button,
|
||||
@@ -259,7 +260,7 @@ h1 {
|
||||
}
|
||||
.directory-add-toggle {
|
||||
flex: 0 0 auto;
|
||||
padding: 5px 8px;
|
||||
padding: 7px 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.directory-add {
|
||||
@@ -269,12 +270,34 @@ h1 {
|
||||
gap: 5px;
|
||||
}
|
||||
.directory-add input {
|
||||
width: 220px;
|
||||
padding: 5px 7px;
|
||||
font-size: 11px;
|
||||
width: 280px;
|
||||
padding: 7px 9px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.directory-add button {
|
||||
padding: 5px 8px;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
.directory-add-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #c4ed8b;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.directory-add-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid #52703a;
|
||||
border-top-color: #c4ed8b;
|
||||
border-radius: 50%;
|
||||
animation: directory-add-spin 0.75s linear infinite;
|
||||
}
|
||||
@keyframes directory-add-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.notifications {
|
||||
@@ -311,9 +334,36 @@ h1 {
|
||||
.workflow-main {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 210px;
|
||||
grid-template-columns: minmax(0, 1fr) 250px;
|
||||
gap: 8px;
|
||||
}
|
||||
.transcript-container {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
}
|
||||
.transcript-container .transcript {
|
||||
height: 100%;
|
||||
}
|
||||
.scroll-to-latest {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 50%;
|
||||
bottom: 14px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border: 1px solid #6f8b56;
|
||||
border-radius: 50%;
|
||||
background: #263020;
|
||||
color: #dcecc8;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.scroll-to-latest:hover {
|
||||
background: #35452b;
|
||||
}
|
||||
.transcript,
|
||||
.settings {
|
||||
min-height: 0;
|
||||
@@ -499,14 +549,14 @@ h1 {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.38;
|
||||
color: #d7dbd5;
|
||||
}
|
||||
.muted {
|
||||
margin: 0;
|
||||
color: #858b85;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.warning {
|
||||
color: #ffb4aa;
|
||||
@@ -519,7 +569,7 @@ h1 {
|
||||
}
|
||||
.pi-controls {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 150px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid #303730;
|
||||
@@ -577,10 +627,10 @@ textarea:focus {
|
||||
outline-offset: 1px;
|
||||
}
|
||||
textarea {
|
||||
min-height: 48px;
|
||||
max-height: 100px;
|
||||
min-height: 64px;
|
||||
max-height: 140px;
|
||||
resize: vertical;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.composer textarea:focus {
|
||||
@@ -739,11 +789,49 @@ textarea {
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-size: 17px;
|
||||
}
|
||||
.settings-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.scale-settings {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
border-top: 1px solid #303830;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.scale-settings-heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.scale-settings-heading output {
|
||||
color: #b8df84;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 800;
|
||||
}
|
||||
.scale-adjustment {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.scale-adjustment button {
|
||||
min-width: 32px;
|
||||
padding: 5px 8px;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
.scale-adjustment button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.scale-adjustment input[type="range"] {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
accent-color: #b8df84;
|
||||
}
|
||||
.session-panel {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -859,7 +947,7 @@ h2 {
|
||||
grid-template-columns: minmax(0, 1fr) 170px;
|
||||
}
|
||||
.pi-controls {
|
||||
grid-template-columns: minmax(0, 1fr) 120px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.transcript {
|
||||
padding: 7px 9px;
|
||||
|
||||
+215
-48
@@ -9,6 +9,7 @@ import {
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import "./App.css";
|
||||
|
||||
type Agent = { id: string; worktreePath: string; state: string };
|
||||
@@ -131,6 +132,10 @@ const thinkingLevels = [
|
||||
"max",
|
||||
];
|
||||
const selectedPathKey = "pi-status-ui.selected-worktree";
|
||||
const uiScaleKey = "pi-status-ui.interface-scale";
|
||||
const minimumUiScale = 0.9;
|
||||
const maximumUiScale = 1.4;
|
||||
const uiScaleStep = 0.05;
|
||||
const controlCommands: Command[] = [
|
||||
{
|
||||
name: "model",
|
||||
@@ -181,6 +186,10 @@ function formatTokens(value?: number | null) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function clampUiScale(value: number) {
|
||||
return Math.min(maximumUiScale, Math.max(minimumUiScale, value));
|
||||
}
|
||||
|
||||
function currentTodos(messages: Message[]) {
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
@@ -221,14 +230,21 @@ function App() {
|
||||
const [state, setState] = useState<AgentState>({});
|
||||
const [stats, setStats] = useState<SessionStats>({});
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [isTranscriptAtBottom, setIsTranscriptAtBottom] = useState(true);
|
||||
const [pendingSubmissions, setPendingSubmissions] = useState<
|
||||
PendingSubmission[]
|
||||
>([]);
|
||||
const [models, setModels] = useState<Model[]>([]);
|
||||
const [commands, setCommands] = useState<Command[]>([]);
|
||||
const [message, setMessage] = useState("");
|
||||
const [uiScale, setUiScale] = useState(() => {
|
||||
const storedScale = Number(localStorage.getItem(uiScaleKey));
|
||||
return Number.isFinite(storedScale) ? clampUiScale(storedScale) : 1;
|
||||
});
|
||||
const [folderPath, setFolderPath] = useState("");
|
||||
const [addingDirectory, setAddingDirectory] = useState(false);
|
||||
const [isAddingDirectory, setIsAddingDirectory] = useState(false);
|
||||
const [pendingDirectoryPath, setPendingDirectoryPath] = useState<string>();
|
||||
const [status, setStatus] = useState("Connecting to Pi Status Bridge…");
|
||||
const [view, setView] = useState<"conversation" | "settings">("conversation");
|
||||
const [commandFollowUp, setCommandFollowUp] = useState<FollowUpCommand>();
|
||||
@@ -253,6 +269,8 @@ function App() {
|
||||
const bridgeRetryTimer = useRef<number | undefined>(undefined);
|
||||
const bridgeRetryAttempt = useRef(0);
|
||||
const lastEventSequence = useRef<Record<string, number>>({});
|
||||
const eventReloadInFlight = useRef(false);
|
||||
const queuedEventReload = useRef<string | undefined>(undefined);
|
||||
const [workProgress, setWorkProgress] = useState<WorkProgress>({
|
||||
phase: "idle",
|
||||
detail: "Ready for your next prompt",
|
||||
@@ -338,6 +356,20 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
function reloadAgentAfterEvent(agentId: string) {
|
||||
if (eventReloadInFlight.current) {
|
||||
queuedEventReload.current = agentId;
|
||||
return;
|
||||
}
|
||||
eventReloadInFlight.current = true;
|
||||
void loadAgent(agentId).finally(() => {
|
||||
eventReloadInFlight.current = false;
|
||||
const queuedAgentId = queuedEventReload.current;
|
||||
queuedEventReload.current = undefined;
|
||||
if (queuedAgentId) reloadAgentAfterEvent(queuedAgentId);
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleBridgeReconnect(preferredPath?: string) {
|
||||
if (bridgeRetryTimer.current !== undefined) return;
|
||||
const delay = Math.min(250 * 2 ** bridgeRetryAttempt.current, 2_000);
|
||||
@@ -402,20 +434,55 @@ function App() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!pendingDirectoryPath ||
|
||||
!directories.some(
|
||||
(directory) => directory.worktreePath === pendingDirectoryPath,
|
||||
)
|
||||
)
|
||||
return;
|
||||
setPendingDirectoryPath(undefined);
|
||||
setIsAddingDirectory(false);
|
||||
setAddingDirectory(false);
|
||||
setStatus(`Added ${worktreeLabel(pendingDirectoryPath)}`);
|
||||
}, [directories, pendingDirectoryPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedId) void loadSessions(selectedId);
|
||||
else setSessions([]);
|
||||
}, [selectedId]);
|
||||
|
||||
function updateTranscriptScrollPosition() {
|
||||
const transcript = transcriptRef.current;
|
||||
if (!transcript) return;
|
||||
const isAtBottom =
|
||||
transcript.scrollHeight -
|
||||
transcript.scrollTop -
|
||||
transcript.clientHeight <=
|
||||
4;
|
||||
setIsTranscriptAtBottom((current) =>
|
||||
current === isAtBottom ? current : isAtBottom,
|
||||
);
|
||||
}
|
||||
|
||||
function scrollTranscriptToLatest() {
|
||||
const transcript = transcriptRef.current;
|
||||
if (!transcript) return;
|
||||
transcript.scrollTop = transcript.scrollHeight;
|
||||
setIsTranscriptAtBottom(true);
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const scrollToLatest = () => {
|
||||
const transcript = transcriptRef.current;
|
||||
if (transcript) transcript.scrollTop = transcript.scrollHeight;
|
||||
};
|
||||
scrollToLatest();
|
||||
const frame = requestAnimationFrame(scrollToLatest);
|
||||
setIsTranscriptAtBottom(true);
|
||||
}, [selectedId]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!isTranscriptAtBottom) return;
|
||||
scrollTranscriptToLatest();
|
||||
const frame = requestAnimationFrame(scrollTranscriptToLatest);
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [messages, pendingSubmissions, selectedId]);
|
||||
}, [isTranscriptAtBottom, messages, pendingSubmissions, selectedId]);
|
||||
|
||||
useEffect(() => {
|
||||
const userMessages = messages.filter((entry) => entry.role === "user");
|
||||
@@ -579,6 +646,7 @@ function App() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
let unlisten: (() => void) | undefined;
|
||||
let unlistenError: (() => void) | undefined;
|
||||
void listen<BridgeEvent>("bridge-event", (event) => {
|
||||
@@ -608,10 +676,11 @@ function App() {
|
||||
);
|
||||
}
|
||||
updateWorkProgress(bridgeEvent);
|
||||
if (selectedId) void loadAgent(selectedId);
|
||||
if (selectedId) void reloadAgentAfterEvent(selectedId);
|
||||
}
|
||||
}).then((stop) => {
|
||||
unlisten = stop;
|
||||
if (disposed) stop();
|
||||
else unlisten = stop;
|
||||
});
|
||||
void listen<{ message?: string }>("bridge-error", (event) => {
|
||||
setStatus(
|
||||
@@ -619,9 +688,11 @@ function App() {
|
||||
);
|
||||
scheduleBridgeReconnect();
|
||||
}).then((stop) => {
|
||||
unlistenError = stop;
|
||||
if (disposed) stop();
|
||||
else unlistenError = stop;
|
||||
});
|
||||
return () => {
|
||||
disposed = true;
|
||||
unlisten?.();
|
||||
unlistenError?.();
|
||||
};
|
||||
@@ -783,28 +854,54 @@ function App() {
|
||||
setCommandFollowUp(undefined);
|
||||
}
|
||||
|
||||
function updateUiScale(nextScale: number) {
|
||||
const scale = clampUiScale(nextScale);
|
||||
setUiScale(scale);
|
||||
localStorage.setItem(uiScaleKey, String(scale));
|
||||
}
|
||||
|
||||
async function activateWorktree(worktreePath: string) {
|
||||
setIsAddingDirectory(true);
|
||||
setPendingDirectoryPath(worktreePath);
|
||||
setStatus("Adding directory…");
|
||||
try {
|
||||
const result = await invoke<{ agent: Agent }>("select_worktree", {
|
||||
worktreePath,
|
||||
});
|
||||
localStorage.setItem(selectedPathKey, worktreePath);
|
||||
const addedPath = result.agent.worktreePath;
|
||||
localStorage.setItem(selectedPathKey, addedPath);
|
||||
setPendingDirectoryPath(addedPath);
|
||||
setFolderPath("");
|
||||
setAddingDirectory(false);
|
||||
setSelectedId(result.agent.id);
|
||||
await refreshAgents(worktreePath);
|
||||
await refreshAgents(addedPath);
|
||||
setView("conversation");
|
||||
} catch (error) {
|
||||
setPendingDirectoryPath(undefined);
|
||||
setIsAddingDirectory(false);
|
||||
setStatus(String(error));
|
||||
}
|
||||
}
|
||||
|
||||
async function browseForFolder() {
|
||||
try {
|
||||
const selectedPath = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
defaultPath: folderPath || undefined,
|
||||
});
|
||||
if (selectedPath) setFolderPath(selectedPath);
|
||||
} catch (error) {
|
||||
setStatus(String(error));
|
||||
}
|
||||
}
|
||||
|
||||
async function addFolder() {
|
||||
if (!folderPath.startsWith("/")) {
|
||||
const worktreePath = folderPath.trim();
|
||||
if (!worktreePath.startsWith("/")) {
|
||||
setStatus("Enter an absolute folder path");
|
||||
return;
|
||||
}
|
||||
await activateWorktree(folderPath);
|
||||
await activateWorktree(worktreePath);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
@@ -940,7 +1037,7 @@ function App() {
|
||||
const workspaceLabel =
|
||||
extensionTitle ?? worktreeLabel(selected?.worktreePath) ?? "Pi";
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<main className="app-shell" style={{ zoom: uiScale }}>
|
||||
<header
|
||||
className="workflow-header"
|
||||
onMouseDown={(event) => {
|
||||
@@ -1069,12 +1166,14 @@ function App() {
|
||||
<button
|
||||
aria-expanded={addingDirectory}
|
||||
className="quiet directory-add-toggle"
|
||||
disabled={isAddingDirectory}
|
||||
onClick={() => setAddingDirectory((current) => !current)}
|
||||
>
|
||||
{addingDirectory ? "Cancel" : "Add directory"}
|
||||
</button>
|
||||
{addingDirectory && (
|
||||
<form
|
||||
aria-busy={isAddingDirectory}
|
||||
className="directory-add"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
@@ -1083,12 +1182,28 @@ function App() {
|
||||
>
|
||||
<input
|
||||
autoFocus
|
||||
disabled={isAddingDirectory}
|
||||
value={folderPath}
|
||||
onChange={(event) => setFolderPath(event.currentTarget.value)}
|
||||
placeholder="/absolute/path/to/project"
|
||||
aria-label="Directory path"
|
||||
/>
|
||||
<button type="submit">Add</button>
|
||||
<button
|
||||
disabled={isAddingDirectory}
|
||||
onClick={() => void browseForFolder()}
|
||||
type="button"
|
||||
>
|
||||
Browse…
|
||||
</button>
|
||||
<button disabled={isAddingDirectory} type="submit">
|
||||
Add
|
||||
</button>
|
||||
{isAddingDirectory && (
|
||||
<span className="directory-add-status" role="status">
|
||||
<span aria-hidden="true" className="directory-add-spinner" />
|
||||
Adding directory…
|
||||
</span>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</section>
|
||||
@@ -1120,6 +1235,44 @@ function App() {
|
||||
{state.messageCount ?? 0} messages ·{" "}
|
||||
{state.pendingMessageCount ?? 0} queued
|
||||
</p>
|
||||
<section
|
||||
aria-labelledby="interface-scale-heading"
|
||||
className="scale-settings"
|
||||
>
|
||||
<div className="scale-settings-heading">
|
||||
<h2 id="interface-scale-heading">Interface scale</h2>
|
||||
<output aria-live="polite">{Math.round(uiScale * 100)}%</output>
|
||||
</div>
|
||||
<div className="scale-adjustment">
|
||||
<button
|
||||
aria-label="Decrease interface scale"
|
||||
disabled={uiScale <= minimumUiScale}
|
||||
onClick={() => updateUiScale(uiScale - uiScaleStep)}
|
||||
type="button"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
aria-label="Interface scale"
|
||||
max={maximumUiScale}
|
||||
min={minimumUiScale}
|
||||
onChange={(event) =>
|
||||
updateUiScale(Number(event.currentTarget.value))
|
||||
}
|
||||
step={uiScaleStep}
|
||||
type="range"
|
||||
value={uiScale}
|
||||
/>
|
||||
<button
|
||||
aria-label="Increase interface scale"
|
||||
disabled={uiScale >= maximumUiScale}
|
||||
onClick={() => updateUiScale(uiScale + uiScaleStep)}
|
||||
type="button"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
className="session-panel"
|
||||
aria-label="Sessions for selected directory"
|
||||
@@ -1201,41 +1354,55 @@ function App() {
|
||||
role="tabpanel"
|
||||
>
|
||||
<div className="workflow-main">
|
||||
<section
|
||||
className={`transcript ${pendingExtension ? "with-extension" : ""} ${workProgress.phase}`}
|
||||
aria-label="Conversation"
|
||||
ref={transcriptRef}
|
||||
>
|
||||
{messages.length ? (
|
||||
messages.map((entry, index) => (
|
||||
<div className="transcript-container">
|
||||
<section
|
||||
className={`transcript ${pendingExtension ? "with-extension" : ""} ${workProgress.phase}`}
|
||||
aria-label="Conversation"
|
||||
onScroll={updateTranscriptScrollPosition}
|
||||
ref={transcriptRef}
|
||||
>
|
||||
{messages.length ? (
|
||||
messages.map((entry, index) => (
|
||||
<article
|
||||
key={index}
|
||||
className={`message ${entry.role ?? "system"}`}
|
||||
>
|
||||
<strong>{entry.role ?? "message"}</strong>
|
||||
<pre>{messageText(entry)}</pre>
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<p className="muted">No messages yet.</p>
|
||||
)}
|
||||
{pendingSubmissions.map((pending) => (
|
||||
<article
|
||||
key={index}
|
||||
className={`message ${entry.role ?? "system"}`}
|
||||
className={`message user pending-message ${pending.phase}`}
|
||||
key={pending.id}
|
||||
>
|
||||
<strong>{entry.role ?? "message"}</strong>
|
||||
<pre>{messageText(entry)}</pre>
|
||||
<strong>
|
||||
You{" "}
|
||||
<span>
|
||||
{pending.phase === "sending"
|
||||
? "Sending…"
|
||||
: "Sent · waiting for Pi"}
|
||||
</span>
|
||||
</strong>
|
||||
<pre>{pending.text}</pre>
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<p className="muted">No messages yet.</p>
|
||||
)}
|
||||
{pendingSubmissions.map((pending) => (
|
||||
<article
|
||||
className={`message user pending-message ${pending.phase}`}
|
||||
key={pending.id}
|
||||
))}
|
||||
</section>
|
||||
{!isTranscriptAtBottom && (
|
||||
<button
|
||||
aria-label="Scroll to latest message"
|
||||
className="scroll-to-latest"
|
||||
onClick={scrollTranscriptToLatest}
|
||||
type="button"
|
||||
>
|
||||
<strong>
|
||||
You{" "}
|
||||
<span>
|
||||
{pending.phase === "sending"
|
||||
? "Sending…"
|
||||
: "Sent · waiting for Pi"}
|
||||
</span>
|
||||
</strong>
|
||||
<pre>{pending.text}</pre>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
<span aria-hidden="true">↓</span>
|
||||
<span className="sr-only">Scroll to latest message</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<aside className="todos-pane" aria-label="Current todos">
|
||||
<div className="todos-heading">
|
||||
<strong>Todos</strong>
|
||||
|
||||
Reference in New Issue
Block a user