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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ new version heading in the same commit.

## [Unreleased]

## [0.222.0] — 2026-07-16
### Added
- **Chat renders KB pages and hosted apps inline too.** Extends the inline-deliverable cards (v0.220.0)
beyond Library artifacts: when an agent writes a Knowledge Base page (`kb_write`) or builds/changes a
hosted app (`app_create`/`app_update`) mid-conversation, the Chat window now shows a titled tile for it
— the KB tile deep-links to `#/kb/<section>/<slug>`, the app tile to `#/apps/<id>` plus an **Open** link
to the live app when it's published. Unlike artifact ids (minted server-side, parsed from the tool
result), the KB `section`/`slug` and app `id` come straight from the tool **input**, captured
optimistically and dropped if the write comes back an error. The `/api/sessions/:id/conversation` route
resolves them into viewer-safe `ChatKbRef`/`ChatAppRef` cards (KB pages and apps are tenant-wide surfaces
every member can already browse; an unknown/deleted ref is dropped). Also gave those tools clearer
activity labels ("Updated the knowledge base", "Built an app", "Updated an app").

## [0.221.0] — 2026-07-16
### Added
- **Session activity trail — every object a run opened, with live status.** The per-session activity
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agent-os",
"version": "0.221.0",
"version": "0.222.0",
"description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.",
"license": "MIT",
"type": "commonjs",
Expand Down
67 changes: 60 additions & 7 deletions src/edge/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,25 @@ export interface ChatArtifactRef {
raw: string;
}

/** A viewer-safe reference to a Knowledge Base page an activity wrote (`kb_write`). The chat UI renders a
* titled tile deep-linking to `#/kb/<section>/<slug>`. Resolved by the /conversation route from the
* activity's `kbRefs`; a page the KB no longer has is dropped. */
export interface ChatKbRef {
section: string;
slug: string;
title: string;
}

/** A viewer-safe reference to a hosted App an activity built or changed (`app_create` / `app_update`). The
* chat UI renders a tile deep-linking to `#/apps/<id>` (and, when published, an "open" link to the live
* app). Resolved by the /conversation route from the activity's `appIds`; an unknown id is dropped. */
export interface ChatAppRef {
id: string;
name: string;
icon?: string;
published: boolean;
}

/** One entry in the human-readable timeline. */
export type ChatTurn =
| { kind: 'user'; text: string; ts: number }
Expand All @@ -46,9 +65,15 @@ export type ChatTurn =
* parsed from the tool result. The /conversation route resolves these to rich {@link
* ChatArtifactRef} cards the chat UI renders inline; unresolvable ids are dropped there. */
artifactIds?: string[];
/** Resolved, viewer-filtered artifact previews (populated by the /conversation route from
* {@link ChatArtifactRef}); absent in the raw transcript parse. */
/** KB page(s) this activity wrote (`kb_write`), captured from the tool INPUT (section/slug). */
kbRefs?: { section: string; slug: string }[];
/** Hosted app id(s) this activity built or changed (`app_create` / `app_update`), from the INPUT. */
appIds?: string[];
/** Resolved, viewer-filtered previews (populated by the /conversation route from {@link
* ChatArtifactRef}/{@link ChatKbRef}/{@link ChatAppRef}); absent in the raw transcript parse. */
artifacts?: ChatArtifactRef[];
kbPages?: ChatKbRef[];
apps?: ChatAppRef[];
ts: number;
};

Expand Down Expand Up @@ -140,8 +165,11 @@ function friendlyTool(name: string, input: Record<string, unknown>): { label: st
if (/^slack_/.test(bare)) return { label: 'Sent a Slack message', detail: clip(input.text ?? input.message ?? input.channel) };
if (/^discord_/.test(bare)) return { label: 'Sent a Discord message', detail: clip(input.content ?? input.message) };
if (/^(remember|recall|revise|forget)$/.test(bare)) return { label: 'Used its memory' };
if (bare === 'kb_write') return { label: 'Updated the knowledge base', detail: clip(input.title ?? input.slug) };
if (/^kb_/.test(bare)) return { label: 'Used the knowledge base', detail: clip(input.query ?? input.slug ?? input.title) };
if (/^task_/.test(bare)) return { label: 'Updated the task board', detail: clip(input.title) };
if (bare === 'app_create') return { label: 'Built an app', detail: clip(input.name ?? input.id) };
if (bare === 'app_update') return { label: 'Updated an app', detail: clip(input.id) };
// Artifact-producing tools: keep a distinct label so the inline artifact card reads naturally under it.
if (bare === 'publish') return { label: 'Published to the Library', detail: clip(input.title ?? basename(input.path)) };
if (bare === 'image_generate') return { label: 'Created an image', detail: clip(input.prompt) };
Expand All @@ -157,6 +185,22 @@ function friendlyTool(name: string, input: Record<string, unknown>): { label: st
/** Bare (mcp-stripped) names of tools whose result yields Library artifact id(s) we render inline. */
const ARTIFACT_TOOLS = new Set(['publish', 'image_generate', 'image_edit', 'video_generate']);

/** From a tool_use's bare name + INPUT, derive the KB / app reference(s) that call SUCCEEDS produces — the
* keys (section/slug, app id) live in the input, so unlike artifact ids we needn't parse the result text.
* Attached to the activity card at tool_use time and cleared if the tool_result comes back an error. */
function refsFromInput(bare: string, input: Record<string, unknown>): { kbRefs?: { section: string; slug: string }[]; appIds?: string[] } {
if (bare === 'kb_write') {
const section = typeof input.section === 'string' ? input.section.trim() : '';
const slug = typeof input.slug === 'string' ? input.slug.trim() : '';
if (section && slug) return { kbRefs: [{ section, slug }] };
}
if (bare === 'app_create' || bare === 'app_update') {
const id = typeof input.id === 'string' ? input.id.trim().toLowerCase() : '';
if (id) return { appIds: [id] };
}
return {};
}

/** Flatten a tool_result's `content` (string, or an array of `{type:'text',text}` blocks) to plain text. */
function resultText(content: unknown): string {
if (typeof content === 'string') return content;
Expand Down Expand Up @@ -221,18 +265,27 @@ export function readConversation(claudeSessionId: string): Conversation {
const text = String(b.text || '').trim();
if (text) turns.push({ kind: o.type === 'user' ? 'user' : 'assistant', text, ts });
} else if (b.type === 'tool_use') {
const { label, detail } = friendlyTool(String(b.name || ''), (b.input || {}) as Record<string, unknown>);
const rawName = String(b.name || '');
const input = (b.input || {}) as Record<string, unknown>;
const { label, detail } = friendlyTool(rawName, input);
activityById.set(String(b.id), turns.length);
turns.push({ kind: 'activity', tool: String(b.name || ''), label, detail, status: 'running', ts });
// KB / app refs come from the INPUT (the section/slug or app id) — attach them optimistically now;
// a failed tool_result clears them below so a broken write never shows a card.
const { kbRefs, appIds } = refsFromInput(bareName(rawName), input);
turns.push({ kind: 'activity', tool: rawName, label, detail, status: 'running', kbRefs, appIds, ts });
} else if (b.type === 'tool_result') {
const idx = activityById.get(String(b.tool_use_id));
if (idx != null) {
const card = turns[idx];
if (card && card.kind === 'activity') {
card.status = b.is_error ? 'error' : 'ok';
// Artifact-producing tool that succeeded → capture the Library id(s) it returned, so the
// route can resolve them into inline preview cards.
if (!b.is_error && ARTIFACT_TOOLS.has(bareName(card.tool))) {
if (b.is_error) {
// The write failed — drop the optimistic KB/app refs so no card is shown.
delete card.kbRefs;
delete card.appIds;
} else if (ARTIFACT_TOOLS.has(bareName(card.tool))) {
// Artifact-producing tool that succeeded → capture the Library id(s) it returned, so the
// route can resolve them into inline preview cards.
const ids = extractArtifactIds(resultText(b.content));
if (ids.length) card.artifactIds = ids;
}
Expand Down
61 changes: 40 additions & 21 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { exampleCapabilities } from './capabilities/examples';
import { evaluate } from './observability/evaluation';
import { TerminalManager, AGENT_OS_OPERATING_NOTES } from './terminal';
import { classifyActivity, clipText, ActivityCategory, ActivityEffect, ActivityTarget } from './state/session-activity';
import { readConversation, type ChatArtifactRef } from './edge/conversation';
import { readConversation, type ChatArtifactRef, type ChatKbRef, type ChatAppRef } from './edge/conversation';
import { summarizeConversation } from './edge/summarize';
import { Automation, Automations, nextCronRun, derivedConcurrencyCap, chatTitle } from './edge/automations';
import { SlackSocket } from './edge/slack-socket';
Expand Down Expand Up @@ -2237,28 +2237,47 @@ async function handle(os: AgentOS, tm: TerminalManager, autos: Automations, req:
if (!tm.canViewSession(id, me)) return sendJson(res, 403, { error: 'not allowed to view this session' });
const claudeId = tm.sessionClaudeId(id);
const convo = claudeId ? readConversation(claudeId) : { turns: [], found: false };
// Resolve any artifact ids an activity produced (publish / image_generate / …) into viewer-safe
// preview cards the chat UI renders inline — dropping ids the viewer may not see or that no longer
// exist. The viewer already passed canViewSession, so their own run's deliverables resolve.
// Resolve the deliverables an activity produced (a Library artifact, a KB page, a hosted app) into
// viewer-safe preview cards the chat UI renders inline — dropping anything the viewer may not see or
// that no longer exists. The viewer already passed canViewSession, so their own run's outputs resolve;
// KB pages and apps are tenant-wide surfaces every member can already browse.
for (const t of convo.turns) {
if (t.kind !== 'activity' || !t.artifactIds?.length) continue;
const refs: ChatArtifactRef[] = [];
for (const aid of t.artifactIds) {
const a = os.artifacts.get(aid);
if (!a) continue;
if (!tm.canViewSpawn(a.source ?? null, me) && !a.sharedTeam) continue;
refs.push({
id: a.id,
title: a.title || a.filename,
kind: a.kind,
mime: a.mime,
filename: a.filename,
isImage: a.mime.startsWith('image/'),
isVideo: a.mime.startsWith('video/'),
raw: `/api/artifacts/${a.id}/raw`,
});
if (t.kind !== 'activity') continue;
if (t.artifactIds?.length) {
const refs: ChatArtifactRef[] = [];
for (const aid of t.artifactIds) {
const a = os.artifacts.get(aid);
if (!a) continue;
if (!tm.canViewSpawn(a.source ?? null, me) && !a.sharedTeam) continue;
refs.push({
id: a.id,
title: a.title || a.filename,
kind: a.kind,
mime: a.mime,
filename: a.filename,
isImage: a.mime.startsWith('image/'),
isVideo: a.mime.startsWith('video/'),
raw: `/api/artifacts/${a.id}/raw`,
});
}
if (refs.length) t.artifacts = refs;
}
if (t.kbRefs?.length) {
const refs: ChatKbRef[] = [];
for (const r of t.kbRefs) {
const page = os.kb.read(os.tenant, r.section, r.slug);
if (page) refs.push({ section: page.section, slug: page.slug, title: page.title });
}
if (refs.length) t.kbPages = refs;
}
if (t.appIds?.length) {
const refs: ChatAppRef[] = [];
for (const aid of t.appIds) {
const app = os.apps.get(aid);
if (app) refs.push({ id: app.id, name: app.name, icon: app.icon, published: !!app.published });
}
if (refs.length) t.apps = refs;
}
if (refs.length) t.artifacts = refs;
}
return sendJson(res, 200, { agent, ...convo });
}
Expand Down
42 changes: 38 additions & 4 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui/dropdown-menu'
import { api, EFFORTS, PERMISSION_MODES, type PermissionMode, type StateResp, type AgentInfo, type Session, type Msg, type Member, type Role, type TeamResp, type MemberIdentity, type IdentityProvider, IDENTITY_PROVIDERS, type Automation, type Task, type TaskEvent, type TaskAttachment, type TaskStatus, type AddTaskReq, type Goal, type GoalEvent, type GoalStatus, type GoalCounts, type GoalProgress, type AddGoalReq, type MemoryRecord, type MemoryHealth, type MemoryBackend, type MemorySettings, type MemorySettingsReq, type OllamaStatus, type KbPage, type KbRevision, type AgentRevision, type AgentStats, type Recommendation, type DigestConfig, type DigestModel, type DreamingState, type Measurement, type Insights, type ImprovementTile, type MemoryCleanupPlan, type KbTidyPlan, type StuckGoal, type TroubledAutomation, type PolicyDocument, type PolicyRule, type PolicyOutcome, type PolicyOp, type PolicyProposal, type PolicyRevision, type DirListing, type FileEntry, type FileContent, type Artifact, type AppInfo, type AppFile, type AppCapabilities, type SkillSummary, type SkillsResp, type CatalogSkill, type CatalogAgent, type SkillSource, type RemoteSkill, type SkillshHit, type SkillRequest, type SecretRequest, type IntegrationsResp, type SlackStatus, type DiscordStatus, type AuditEvent, type Effort, type RuntimeTuning, type Concurrency, type SecretMeta, type UpdateStatus, type UpdateApplyResult, type ActivityEvent, type ActivitySummaryRow, type SystemMetrics, type DepsReport, type DepStatus, type DepsInstallResult, type ChatTurn, type ChatArtifactRef } from '@/lib/api'
import { api, EFFORTS, PERMISSION_MODES, type PermissionMode, type StateResp, type AgentInfo, type Session, type Msg, type Member, type Role, type TeamResp, type MemberIdentity, type IdentityProvider, IDENTITY_PROVIDERS, type Automation, type Task, type TaskEvent, type TaskAttachment, type TaskStatus, type AddTaskReq, type Goal, type GoalEvent, type GoalStatus, type GoalCounts, type GoalProgress, type AddGoalReq, type MemoryRecord, type MemoryHealth, type MemoryBackend, type MemorySettings, type MemorySettingsReq, type OllamaStatus, type KbPage, type KbRevision, type AgentRevision, type AgentStats, type Recommendation, type DigestConfig, type DigestModel, type DreamingState, type Measurement, type Insights, type ImprovementTile, type MemoryCleanupPlan, type KbTidyPlan, type StuckGoal, type TroubledAutomation, type PolicyDocument, type PolicyRule, type PolicyOutcome, type PolicyOp, type PolicyProposal, type PolicyRevision, type DirListing, type FileEntry, type FileContent, type Artifact, type AppInfo, type AppFile, type AppCapabilities, type SkillSummary, type SkillsResp, type CatalogSkill, type CatalogAgent, type SkillSource, type RemoteSkill, type SkillshHit, type SkillRequest, type SecretRequest, type IntegrationsResp, type SlackStatus, type DiscordStatus, type AuditEvent, type Effort, type RuntimeTuning, type Concurrency, type SecretMeta, type UpdateStatus, type UpdateApplyResult, type ActivityEvent, type ActivitySummaryRow, type SystemMetrics, type DepsReport, type DepStatus, type DepsInstallResult, type ChatTurn, type ChatArtifactRef, type ChatKbRef, type ChatAppRef } from '@/lib/api'
import { type Branding, type PublicBranding, type NotificationPrefs, DEFAULT_NOTIFICATION_PREFS, type PromptShortcut } from '@/lib/api'
import { applyAccent, applyFavicon, faviconDataUri, readableOn } from '@/lib/branding'
import { ConnectorsPage, GithubMineCard } from '@/connectors'
Expand Down Expand Up @@ -3061,21 +3061,55 @@ function ArtifactCard({ a }: { a: ChatArtifactRef }) {
)
}

/** A KB page an agent wrote, as an inline tile deep-linking to `#/kb/<section>/<slug>`. */
function KbCard({ p }: { p: ChatKbRef }) {
return (
<a href={navHref('kb', `${p.section}/${p.slug}`)} className="group flex max-w-[20rem] items-center gap-2.5 rounded-xl border bg-card px-3 py-2 transition hover:border-primary">
<BookText className="h-5 w-5 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium" title={p.title}>{p.title}</span>
<span className="block truncate text-[11px] text-muted-foreground">{p.section}/{p.slug}</span>
</span>
<ExternalLink className="h-3.5 w-3.5 shrink-0 text-muted-foreground opacity-0 transition group-hover:opacity-100" />
</a>
)
}

/** A hosted app an agent built or changed — a tile into the Apps console, plus an "Open" link to the live
* app when it's published (a proposed app isn't routable yet, so it only deep-links for review). */
function AppCard({ a }: { a: ChatAppRef }) {
return (
<div className="flex max-w-[20rem] items-center gap-2.5 rounded-xl border bg-card px-3 py-2">
<AgentIcon icon={a.icon} className="h-5 w-5 shrink-0 text-muted-foreground" />
<a href={navHref('apps', a.id)} className="group min-w-0 flex-1">
<span className="block truncate text-sm font-medium group-hover:underline" title={a.name}>{a.name}</span>
<span className="block truncate text-[11px] text-muted-foreground">{a.published ? 'Published app' : 'Proposed — awaiting review'}</span>
</a>
{a.published
? <a href={api.appUrl(a.id)} target="_blank" rel="noreferrer" className="flex shrink-0 items-center gap-1 text-[11px] font-medium text-primary hover:underline"><ExternalLink className="h-3 w-3" /> Open</a>
: <Package className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />}
</div>
)
}

/** One friendly activity card ("Sent a Slack message" ✓). Terminal detail stays hidden by default. An
* artifact-producing activity (publish / image_generate / …) shows its deliverable(s) inline beneath. */
* activity that produced a deliverable (a Library artifact, a KB page, a hosted app) shows it inline beneath. */
function ActivityCard({ turn }: { turn: Extract<ChatTurn, { kind: 'activity' }> }) {
const dot =
turn.status === 'error' ? 'bg-destructive' : turn.status === 'ok' ? 'bg-emerald-500' : 'bg-amber-400 animate-pulse'
const hasCards = !!(turn.artifacts?.length || turn.kbPages?.length || turn.apps?.length)
return (
<div className="space-y-1.5">
<div className="flex items-center gap-2 pl-1 text-[12px] text-muted-foreground">
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${dot}`} />
<span className="font-medium">{turn.label}</span>
{turn.detail && <span className="truncate font-mono text-[11px] opacity-70" title={turn.detail}>{turn.detail}</span>}
</div>
{turn.artifacts && turn.artifacts.length > 0 && (
{hasCards && (
<div className="flex flex-wrap gap-2 pl-3.5">
{turn.artifacts.map((a) => <ArtifactCard key={a.id} a={a} />)}
{turn.artifacts?.map((a) => <ArtifactCard key={a.id} a={a} />)}
{turn.kbPages?.map((p) => <KbCard key={`${p.section}/${p.slug}`} p={p} />)}
{turn.apps?.map((a) => <AppCard key={a.id} a={a} />)}
</div>
)}
</div>
Expand Down
Loading
Loading