Refactor agent architecture and add session storage

Major architectural improvements:
- Renamed AgentSession → Agent (state/ → agent/)
- Removed id field from AgentState
- Fixed transport abstraction to pass messages directly instead of using callbacks
- Eliminated circular dependencies in transport creation

Transport changes:
- Changed signature: run(messages, userMessage, config, signal)
- Removed getMessages callback from ProviderTransport and AppTransport
- Transports now filter attachments internally

Session storage:
- Added SessionRepository with IndexedDB backend
- Auto-save sessions after first exchange
- Auto-generate titles from first user message
- Session list dialog with search and delete
- Persistent storage permission dialog
- Browser extension now auto-loads last session

UI improvements:
- ChatPanel creates single AgentInterface instance in setAgent()
- Added drag & drop file upload to MessageEditor
- Fixed artifacts panel auto-opening on session load
- Added "Drop files here" i18n strings
- Changed "Continue Without Saving" → "Continue Anyway"

Web example:
- Complete rewrite of main.ts with clean architecture
- Added check script to package.json
- Session management with URL state
- Editable session titles

Browser extension:
- Added full session storage support
- History and new session buttons
- Auto-load most recent session on open
- Session titles in header
This commit is contained in:
Mario Zechner 2025-10-06 12:47:52 +02:00
parent c18923a8c5
commit e5cf25a267
23 changed files with 1787 additions and 289 deletions

View file

@ -1,29 +1,28 @@
import { Badge, html } from "@mariozechner/mini-lit";
import { type AgentTool, getModel } from "@mariozechner/pi-ai";
import { LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import type { AgentInterface } from "./components/AgentInterface.js";
import "./components/AgentInterface.js";
import { AgentSession, type AgentSessionState, type ThinkingLevel } from "./state/agent-session.js";
import type { Agent } from "./agent/agent.js";
import { ArtifactsPanel } from "./tools/artifacts/index.js";
import { createJavaScriptReplTool } from "./tools/javascript-repl.js";
import { registerToolRenderer } from "./tools/renderer-registry.js";
import { getAuthToken } from "./utils/auth-token.js";
import { i18n } from "./utils/i18n.js";
const BREAKPOINT = 800; // px - switch between overlay and side-by-side
@customElement("pi-chat-panel")
export class ChatPanel extends LitElement {
@state() private session!: AgentSession;
@state() private artifactsPanel!: ArtifactsPanel;
@state() private agent?: Agent;
@state() private agentInterface?: AgentInterface;
@state() private artifactsPanel?: ArtifactsPanel;
@state() private hasArtifacts = false;
@state() private artifactCount = 0;
@state() private showArtifactsPanel = false;
@state() private windowWidth = window.innerWidth;
@property({ type: String }) systemPrompt = "You are a helpful AI assistant.";
@property({ type: Array }) additionalTools: AgentTool<any, any>[] = [];
@property({ attribute: false }) sandboxUrlProvider?: () => string;
@property({ attribute: false }) onApiKeyRequired?: (provider: string) => Promise<boolean>;
@property({ attribute: false }) additionalTools?: any[];
private resizeHandler = () => {
this.windowWidth = window.innerWidth;
@ -34,19 +33,33 @@ export class ChatPanel extends LitElement {
return this;
}
override async connectedCallback() {
override connectedCallback() {
super.connectedCallback();
// Listen to window resize
window.addEventListener("resize", this.resizeHandler);
// Ensure panel fills height and allows flex layout
this.style.display = "flex";
this.style.flexDirection = "column";
this.style.height = "100%";
this.style.minHeight = "0";
}
// Create JavaScript REPL tool with attachments provider
override disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("resize", this.resizeHandler);
}
async setAgent(agent: Agent) {
this.agent = agent;
// Create AgentInterface
this.agentInterface = document.createElement("agent-interface") as AgentInterface;
this.agentInterface.session = agent;
this.agentInterface.enableAttachments = true;
this.agentInterface.enableModelSelector = true;
this.agentInterface.enableThinkingSelector = true;
this.agentInterface.showThemeToggle = false;
this.agentInterface.onApiKeyRequired = this.onApiKeyRequired;
// Create JavaScript REPL tool
const javascriptReplTool = createJavaScriptReplTool();
if (this.sandboxUrlProvider) {
javascriptReplTool.sandboxUrlProvider = this.sandboxUrlProvider;
@ -59,11 +72,10 @@ export class ChatPanel extends LitElement {
}
registerToolRenderer("artifacts", this.artifactsPanel);
// Attachments provider for both REPL and artifacts
// Attachments provider
const getAttachments = () => {
// Get all attachments from conversation messages
const attachments: any[] = [];
for (const message of this.session.state.messages) {
for (const message of this.agent!.state.messages) {
if (message.role === "user") {
const content = Array.isArray(message.content) ? message.content : [message.content];
for (const block of content) {
@ -86,12 +98,10 @@ export class ChatPanel extends LitElement {
this.artifactsPanel.attachmentsProvider = getAttachments;
this.artifactsPanel.onArtifactsChange = () => {
const count = this.artifactsPanel.artifacts?.size ?? 0;
const count = this.artifactsPanel?.artifacts?.size ?? 0;
const created = count > this.artifactCount;
this.hasArtifacts = count > 0;
this.artifactCount = count;
// Auto-open when new artifacts are created
if (this.hasArtifacts && created) {
this.showArtifactsPanel = true;
}
@ -108,48 +118,27 @@ export class ChatPanel extends LitElement {
this.requestUpdate();
};
const initialState = {
systemPrompt: this.systemPrompt,
model: getModel("anthropic", "claude-sonnet-4-5-20250929"),
tools: [...this.additionalTools, javascriptReplTool, this.artifactsPanel.tool],
thinkingLevel: "off" as ThinkingLevel,
messages: [],
} satisfies Partial<AgentSessionState>;
// initialState = { ...initialState, ...(simpleHtml as any) };
// initialState = { ...initialState, ...(longSession as any) };
// Set tools on the agent
const tools = [javascriptReplTool, this.artifactsPanel.tool, ...(this.additionalTools || [])];
this.agent.setTools(tools);
// Create agent session first so attachments provider works
this.session = new AgentSession({
initialState,
authTokenProvider: async () => getAuthToken(),
transportMode: "provider", // Use provider mode by default (API keys from storage, optional CORS proxy)
});
// Reconstruct artifacts from existing messages
// Temporarily disable the onArtifactsChange callback to prevent auto-opening on load
const originalCallback = this.artifactsPanel.onArtifactsChange;
this.artifactsPanel.onArtifactsChange = undefined;
await this.artifactsPanel.reconstructFromMessages(this.agent.state.messages);
this.artifactsPanel.onArtifactsChange = originalCallback;
// Reconstruct artifacts panel from initial messages (session must exist first)
await this.artifactsPanel.reconstructFromMessages(initialState.messages);
this.hasArtifacts = this.artifactsPanel.artifacts.size > 0;
}
this.artifactCount = this.artifactsPanel.artifacts.size;
override disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("resize", this.resizeHandler);
}
// Expose method to toggle artifacts panel
public toggleArtifactsPanel() {
this.showArtifactsPanel = !this.showArtifactsPanel;
this.requestUpdate();
}
// Check if artifacts panel is currently visible
public get artifactsPanelVisible(): boolean {
return this.showArtifactsPanel;
}
render() {
if (!this.session) {
if (!this.agent || !this.agentInterface) {
return html`<div class="flex items-center justify-center h-full">
<div class="text-muted-foreground">Loading...</div>
<div class="text-muted-foreground">No agent set</div>
</div>`;
}
@ -164,15 +153,7 @@ export class ChatPanel extends LitElement {
return html`
<div class="relative w-full h-full overflow-hidden flex">
<div class="h-full" style="${!isMobile && this.showArtifactsPanel && this.hasArtifacts ? "width: 50%;" : "width: 100%;"}">
<agent-interface
.session=${this.session}
.enableAttachments=${true}
.enableModelSelector=${true}
.showThinkingSelector=${true}
.showThemeToggle=${false}
.showDebugToggle=${false}
.onApiKeyRequired=${this.onApiKeyRequired}
></agent-interface>
${this.agentInterface}
</div>
<!-- Floating pill when artifacts exist and panel is collapsed -->