feat(rpc): add validated desktop Pi RPC command palette
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import type { Model, RuntimeView, TodoTask } from "../workspace/types";
|
||||
import { rpcCommandCatalog, type RpcCommand } from "../commands/rpc";
|
||||
import { CommandPalette } from "./CommandPalette";
|
||||
|
||||
export function messageText(message: RuntimeView["messages"][number]) {
|
||||
if (typeof message.content === "string") return message.content;
|
||||
@@ -13,6 +15,39 @@ export function messageText(message: RuntimeView["messages"][number]) {
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
function TranscriptMessage({
|
||||
message,
|
||||
}: {
|
||||
message: RuntimeView["messages"][number];
|
||||
}) {
|
||||
const role = message.role ?? "system";
|
||||
if (role === "toolResult")
|
||||
return (
|
||||
<article className={`message ${role}`}>
|
||||
<details className="tool-result">
|
||||
<summary>Tool result · {message.toolName ?? "tool"}</summary>
|
||||
<pre>{messageText(message)}</pre>
|
||||
</details>
|
||||
</article>
|
||||
);
|
||||
return (
|
||||
<article className={`message ${role}`}>
|
||||
<strong>{message.role ?? "message"}</strong>
|
||||
<pre>{messageText(message)}</pre>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function pendingStatus(pending: RuntimeView["pendingSubmissions"][number]) {
|
||||
if (pending.phase === "sending")
|
||||
return pending.delivery === "follow_up"
|
||||
? "Queueing follow-up…"
|
||||
: "Sending prompt…";
|
||||
return pending.delivery === "follow_up"
|
||||
? "Follow-up queued · waiting for Pi"
|
||||
: "Prompt accepted · waiting for Pi";
|
||||
}
|
||||
|
||||
function todos(messages: RuntimeView["messages"]) {
|
||||
const order: Record<TodoTask["status"], number> = {
|
||||
in_progress: 0,
|
||||
@@ -41,6 +76,8 @@ export function ConversationWorkspace({
|
||||
onModel,
|
||||
onThinking,
|
||||
isSubmitting = false,
|
||||
isAborting = false,
|
||||
onCommand,
|
||||
}: {
|
||||
runtime: RuntimeView;
|
||||
onDraft: (value: string) => void;
|
||||
@@ -50,13 +87,16 @@ export function ConversationWorkspace({
|
||||
onModel: (model: Model) => void;
|
||||
onThinking: (level: string) => void;
|
||||
isSubmitting?: boolean;
|
||||
isAborting?: boolean;
|
||||
onCommand: (command: RpcCommand) => void;
|
||||
}) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const tasks = useMemo(() => todos(runtime.messages), [runtime.messages]);
|
||||
const runtimeIsWorking =
|
||||
runtime.summary.state === "streaming" ||
|
||||
runtime.progress.phase === "working";
|
||||
const isWorking =
|
||||
isSubmitting ||
|
||||
runtime.progress.phase === "working" ||
|
||||
runtime.progress.phase === "recovering";
|
||||
isSubmitting || runtimeIsWorking || runtime.progress.phase === "recovering";
|
||||
useEffect(() => {
|
||||
if (runtime.scroll.atBottom && ref.current)
|
||||
ref.current.scrollTop = ref.current.scrollHeight;
|
||||
@@ -108,13 +148,7 @@ export function ConversationWorkspace({
|
||||
<p className="muted">No messages yet.</p>
|
||||
)}
|
||||
{runtime.messages.map((message, index) => (
|
||||
<article
|
||||
className={`message ${message.role ?? "system"}`}
|
||||
key={index}
|
||||
>
|
||||
<strong>{message.role ?? "message"}</strong>
|
||||
<pre>{messageText(message)}</pre>
|
||||
</article>
|
||||
<TranscriptMessage key={index} message={message} />
|
||||
))}
|
||||
{runtime.pendingSubmissions.map((pending) => (
|
||||
<article
|
||||
@@ -122,12 +156,7 @@ export function ConversationWorkspace({
|
||||
key={pending.id}
|
||||
>
|
||||
<strong>
|
||||
You{" "}
|
||||
<span>
|
||||
{pending.phase === "sending"
|
||||
? "Sending…"
|
||||
: "Sent · waiting for Pi"}
|
||||
</span>
|
||||
You <span>{pendingStatus(pending)}</span>
|
||||
</strong>
|
||||
<pre>{pending.text}</pre>
|
||||
</article>
|
||||
@@ -215,6 +244,11 @@ export function ConversationWorkspace({
|
||||
{runtime.progress.tool && <small>{runtime.progress.tool}</small>}
|
||||
</div>
|
||||
<div className="composer">
|
||||
<CommandPalette
|
||||
draft={runtime.draft}
|
||||
commands={rpcCommandCatalog}
|
||||
onSelect={onCommand}
|
||||
/>
|
||||
<textarea
|
||||
aria-label="Prompt"
|
||||
value={runtime.draft}
|
||||
@@ -229,9 +263,14 @@ export function ConversationWorkspace({
|
||||
placeholder="Ask Pi…"
|
||||
/>
|
||||
<div className="composer-actions">
|
||||
{runtime.state.isStreaming && (
|
||||
<button type="button" className="quiet" onClick={onAbort}>
|
||||
Abort
|
||||
{runtimeIsWorking && (
|
||||
<button
|
||||
type="button"
|
||||
className="quiet"
|
||||
disabled={isAborting}
|
||||
onClick={onAbort}
|
||||
>
|
||||
{isAborting ? "Aborting…" : "Abort"}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user