Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,12 @@ export default function AppLayout() {
});
}, [refreshActiveServerSession]);

const onApiAuthError = useCallback(async () => {
// Token expired on an API call (prompt, steer, etc.) — refresh and retry
return refreshActiveServerSession();
const onApiAuthError = useCallback(async (): Promise<string | null> => {
const ok = await refreshActiveServerSession();
if (!ok) return null;
const state = useAuthStore.getState();
const sid = state.activeServerId;
return sid ? state.tokens[sid]?.accessToken ?? null : null;
}, [refreshActiveServerSession]);

const piClientConfig = useMemo<PiClientConfig>(
Expand Down
20 changes: 18 additions & 2 deletions app/(app)/chat/[sessionId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import { ExtensionUiDialog } from '@/features/agent/components/extension-ui-dial
import { DiffPanelProvider } from '@/features/agent/components/diff-panel/context';
import { MobileDiffSheetProvider } from '@/features/agent/components/message-list/mobile-diff-sheet';
import { useAgentSession, useChatSessions, useConnection } from '@pi-ui/client';
import type { ImageContent } from '@pi-ui/client';
import { useChatStore } from '@/features/chat/store';
import { useWorkspaceStore } from '@/features/workspace/store';
import type { PendingExtensionUiRequest as LegacyPendingUiRequest } from '@/features/agent/extension-ui';
import type { Attachment } from '@/features/workspace/components/prompt-input/constants';

export default function ChatSessionScreen() {
const { sessionId } = useLocalSearchParams<{ sessionId: string }>();
Expand Down Expand Up @@ -51,10 +53,24 @@ export default function ChatSessionScreen() {
}, [sessionId, selectSession, registerSessionWorkspace]);

const handleSend = useCallback(
async (text: string, _attachments: unknown[], options?: { queueBehavior?: 'steer' | 'followUp' }) => {
async (text: string, attachments: Attachment[], options?: { queueBehavior?: 'steer' | 'followUp' }) => {
if (!sessionId || inputBlocked) return;
setAlertMessage(null);

let images: ImageContent[] | undefined;
const imageAttachments = attachments.filter((a) => a.type === "image" && a.preview);
if (imageAttachments.length > 0) {
images = imageAttachments.map((a) => {
const dataUrl = a.preview!;
const commaIdx = dataUrl.indexOf(",");
const meta = dataUrl.slice(0, commaIdx);
const base64 = dataUrl.slice(commaIdx + 1);
const mimeMatch = meta.match(/data:([^;]+)/);
const mimeType = mimeMatch?.[1] ?? "image/png";
return { type: "image" as const, data: base64, mimeType };
});
}

const isFirst = !session.messages.length;
const behavior = options?.queueBehavior ?? (session.isStreaming ? 'steer' : undefined);
const sendFn = behavior === 'steer'
Expand All @@ -64,7 +80,7 @@ export default function ChatSessionScreen() {
: session.prompt;

try {
await sendFn(text);
await sendFn(text, images ? { images } : undefined);
if (isFirst) setTimeout(() => invalidateChatSessions(), 2000);
} catch (error) {
setAlertMessage(error instanceof Error ? error.message : 'Failed to send prompt');
Expand Down
38 changes: 16 additions & 22 deletions app/(app)/servers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ function ServerFormFields({
);
}

type ServerFormData = Omit<Server, "id"> & { username: string; password: string };

function ServerFormDesktopModal({
visible,
onClose,
Expand All @@ -173,7 +175,7 @@ function ServerFormDesktopModal({
}: {
visible: boolean;
onClose: () => void;
onSave: (data: Omit<Server, "id">) => void;
onSave: (data: ServerFormData) => void;
initial?: Server;
isDark: boolean;
loading?: boolean;
Expand All @@ -189,8 +191,8 @@ function ServerFormDesktopModal({
if (visible) {
setName(initial?.name ?? "");
setAddress(initial?.address ?? "");
setUsername(initial?.username ?? "");
setPassword(initial?.password ?? "");
setUsername("");
setPassword("");
setShowPassword(false);
}
}, [visible, initial]);
Expand Down Expand Up @@ -301,7 +303,7 @@ function ServerFormSheet({
}: {
visible: boolean;
onClose: () => void;
onSave: (data: Omit<Server, "id">) => void;
onSave: (data: ServerFormData) => void;
initial?: Server;
isDark: boolean;
loading?: boolean;
Expand Down Expand Up @@ -336,8 +338,8 @@ function ServerFormSheet({
if (visible) {
setName(initial?.name ?? "");
setAddress(initial?.address ?? "");
setUsername(initial?.username ?? "");
setPassword(initial?.password ?? "");
setUsername("");
setPassword("");
setShowPassword(false);
translateY.value = withTiming(0, TIMING_CONFIG);
overlayOpacity.value = withTiming(1, TIMING_CONFIG);
Expand Down Expand Up @@ -539,7 +541,7 @@ function ServerFormSheet({
function ServerFormModal(props: {
visible: boolean;
onClose: () => void;
onSave: (data: Omit<Server, "id">) => void;
onSave: (data: ServerFormData) => void;
initial?: Server;
isDark: boolean;
loading?: boolean;
Expand Down Expand Up @@ -636,24 +638,16 @@ export default function ServersScreen() {

let connected = false;

// Has stored token — activate and verify session
if (auth.hasToken(server.id)) {
connected = await auth.activateServer(server);
}

// Try login with stored credentials
if (!connected && server.username) {
const result = await auth.loginToServer(server);
connected = result.success;
}

if (connected) {
router.replace('/');
} else {
// No valid token and no credentials — prompt to edit/re-enter credentials
setConnecting(null);
setEditingServer(server);
setLoginError("Not connected. Enter credentials to connect.");
setLoginError("Session expired. Enter credentials to reconnect.");
setFormVisible(true);
}
} catch (e) {
Expand All @@ -664,22 +658,22 @@ export default function ServersScreen() {
};

const handleSave = useCallback(
async (data: Omit<Server, "id">) => {
async (data: Omit<Server, "id"> & { username: string; password: string }) => {
setLoginLoading(true);
setLoginError(null);

const { username, password, ...serverData } = data;
let server: Server;
if (editingServer) {
await updateServer(editingServer.id, data);
server = { ...editingServer, ...data };
await updateServer(editingServer.id, serverData);
server = { ...editingServer, ...serverData };
} else {
await addServer(data);
// get the newly added server (last in list after addServer)
await addServer(serverData);
const servers = useServersStore.getState().servers;
server = servers[servers.length - 1];
}

const result = await loginToServer(server);
const result = await loginToServer(server, { username, password });
setLoginLoading(false);

if (result.success) {
Expand Down
20 changes: 18 additions & 2 deletions app/(app)/workspace/[workspaceId]/s/[sessionId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import { DiffPanelProvider } from "@/features/agent/components/diff-panel/contex
import { DiffSidebar } from "@/features/agent/components/diff-panel";
import { MobileDiffSheetProvider } from "@/features/agent/components/message-list/mobile-diff-sheet";
import { useAgentSession, useConnection, useWorkspaceSessions as useSessions } from "@pi-ui/client";
import type { ImageContent } from "@pi-ui/client";
import { requestBrowserNotificationPermission } from "@/features/agent/browser-notifications";
import type { PendingExtensionUiRequest as LegacyPendingUiRequest } from "@/features/agent/extension-ui";
import type { ChatMessage } from "@/features/agent/types";
import type { Attachment } from "@/features/workspace/components/prompt-input/constants";

export default function SessionScreen() {
const { workspaceId, sessionId } = useLocalSearchParams<{
Expand Down Expand Up @@ -76,13 +78,27 @@ export default function SessionScreen() {
const handleSend = useCallback(
async (
text: string,
_attachments: unknown[],
attachments: Attachment[],
options?: { queueBehavior?: "steer" | "followUp" },
) => {
if (!sessionId || inputBlockedByConnection) return;
setAlertMessage(null);
requestBrowserNotificationPermission();

let images: ImageContent[] | undefined;
const imageAttachments = attachments.filter((a) => a.type === "image" && a.preview);
if (imageAttachments.length > 0) {
images = imageAttachments.map((a) => {
const dataUrl = a.preview!;
const commaIdx = dataUrl.indexOf(",");
const meta = dataUrl.slice(0, commaIdx);
const base64 = dataUrl.slice(commaIdx + 1);
const mimeMatch = meta.match(/data:([^;]+)/);
const mimeType = mimeMatch?.[1] ?? "image/png";
return { type: "image" as const, data: base64, mimeType };
});
}

const behavior = options?.queueBehavior ?? (agentSession.isStreaming ? "steer" : undefined);
const sendFn = behavior === "steer"
? agentSession.steer
Expand All @@ -91,7 +107,7 @@ export default function SessionScreen() {
: agentSession.prompt;

try {
await sendFn(text);
await sendFn(text, images ? { images } : undefined);
} catch (error) {
setAlertMessage(
error instanceof Error ? error.message : "Failed to send prompt",
Expand Down
2 changes: 0 additions & 2 deletions app/connect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ export default function DirectConnectScreen() {
id: serverId,
name: existingServer?.name || connectParams.hostname || "Pico Server",
address: baseUrl,
username: existingServer?.username ?? "",
password: existingServer?.password ?? "",
});

await fetchWorkspaces();
Expand Down
Binary file modified assets/images/android-icon-background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/images/android-icon-foreground.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/images/android-icon-monochrome.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/images/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/images/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/images/splash-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 21 additions & 7 deletions backend/src/models/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,16 +167,30 @@ pub struct WsStreamQuery {
}

#[derive(Debug, Deserialize, ToSchema)]
pub struct SessionStreamQuery {
pub last_message_id: Option<String>,
pub before: Option<String>,
pub limit: Option<u32>,
}
pub struct SessionStreamQuery {}

#[derive(Debug, Deserialize)]
pub struct WsSessionStreamQuery {
pub last_message_id: Option<String>,
pub access_token: Option<String>,
}

#[derive(Debug, Deserialize, ToSchema)]
pub struct SetActiveSessionRequest {
pub connection_id: String,
pub session_id: Option<String>,
pub from_event_id: Option<u64>,
pub from_delta_event_id: Option<u64>,
}

#[derive(Debug, Deserialize, ToSchema)]
pub struct SessionHistoryQuery {
pub before: Option<String>,
pub limit: Option<u32>,
pub access_token: Option<String>,
}

#[derive(Debug, Serialize, ToSchema)]
pub struct SessionHistoryResponse {
pub messages: Vec<Value>,
pub has_more: bool,
pub oldest_entry_id: Option<String>,
}
Loading
Loading