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
5 changes: 1 addition & 4 deletions packages/patterns/chatbot-outliner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,7 @@ const handleCharmLinkClick = handler<

function getMentionable() {
return derive<MentionableCharm[], MentionableCharm[]>(
wish<MentionableCharm[]>(
"#mentionable",
[],
),
wish<MentionableCharm[]>("#mentionable"),
(i) => i,
);
}
Expand Down
52 changes: 26 additions & 26 deletions packages/patterns/chatbot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
llmDialog,
NAME,
navigateTo,
Opaque,
recipe,
Stream,
UI,
Expand All @@ -20,8 +19,8 @@ import {
} from "commontools";
import { type MentionableCharm } from "./backlinks-index.tsx";

function schemaifyWish<T>(path: string, def: Opaque<T>) {
return derive<T, T>(wish<T>(path, def), (i) => i);
function schemaifyWish<T>(path: string) {
return derive<T, T>(wish<T>(path), (i) => i);
}

const addAttachment = handler<
Expand Down Expand Up @@ -274,10 +273,7 @@ export default recipe<ChatInput, ChatOutput>(
({ messages, tools, theme }) => {
const model = cell<string>("anthropic:claude-sonnet-4-5");
const allAttachments = cell<Array<PromptAttachment>>([]);
const mentionable = schemaifyWish<MentionableCharm[]>(
"#mentionable",
[],
);
const mentionable = schemaifyWish<MentionableCharm[]>("#mentionable");

// Derive tools from attachments
const dynamicTools = derive(allAttachments, (attachments) => {
Expand Down Expand Up @@ -358,25 +354,28 @@ export default recipe<ChatInput, ChatOutput>(
const title = TitleGenerator({ model, messages });

const promptInput = (
<div slot="footer">
<ct-prompt-input
placeholder="Ask the LLM a question..."
pending={pending}
$mentionable={mentionable}
onct-send={sendMessage({ addMessage, allAttachments })}
onct-stop={cancelGeneration}
onct-attachment-add={addAttachment({ allAttachments })}
onct-attachment-remove={removeAttachment({ allAttachments })}
/>
<ct-select
items={items}
$value={model}
/>
</div>
<ct-prompt-input
slot="footer"
placeholder="Ask the LLM a question..."
pending={pending}
$mentionable={mentionable}
modelItems={items}
$model={model}
onct-send={sendMessage({ addMessage, allAttachments })}
onct-stop={cancelGeneration}
onct-attachment-add={addAttachment({ allAttachments })}
onct-attachment-remove={removeAttachment({ allAttachments })}
/>
);

const chatLog = (
<ct-vscroll flex showScrollbar fadeEdges snapToBottom>
<ct-vscroll
style="padding: 1rem;"
flex
showScrollbar
fadeEdges
snapToBottom
>
<ct-chat
theme={theme}
$messages={messages}
Expand All @@ -387,14 +386,15 @@ export default recipe<ChatInput, ChatOutput>(
);

const attachmentsAndTools = (
<ct-hstack gap="normal">
<ct-hstack align="center" gap="1">
<ct-attachments-bar
attachments={allAttachments}
removable
onct-remove={removeAttachment({ allAttachments })}
/>
<ct-tools-chip tools={flattenedTools} />
<button
<ct-button
variant="pill"
type="button"
title="Clear chat"
onClick={clearChat({
Expand All @@ -403,7 +403,7 @@ export default recipe<ChatInput, ChatOutput>(
})}
>
Clear
</button>
</ct-button>
</ct-hstack>
);

Expand Down
130 changes: 23 additions & 107 deletions packages/patterns/default-app.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
/// <cts-enable />
import {
BuiltInLLMMessage,
Cell,
cell,
derive,
handler,
ifElse,
lift,
NAME,
navigateTo,
patternTool,
recipe,
str,
UI,
Expand All @@ -21,7 +16,7 @@ import ChatbotOutliner from "./chatbot-outliner.tsx";
import { default as Note } from "./note.tsx";
import BacklinksIndex, { type MentionableCharm } from "./backlinks-index.tsx";
import ChatList from "./chatbot-list-view.tsx";
import { calculator, readWebpage, searchWeb } from "./common-tools.tsx";
import OmniboxFAB from "./omnibox-fab.tsx";

type MinimalCharm = {
[NAME]?: string;
Expand Down Expand Up @@ -61,10 +56,16 @@ const removeCharm = handler<
console.log("charmListCopy before", charmListCopy.length);
charmListCopy.splice(index, 1);
console.log("charmListCopy after", charmListCopy.length);
state.allCharms.resolveAsCell().set(charmListCopy);
state.allCharms.set(charmListCopy);
}
});

const toggleFab = handler<any, { fabExpanded: Cell<boolean> }>(
(_, { fabExpanded }) => {
fabExpanded.set(!fabExpanded.get());
},
);

const spawnChatList = handler<void, void>((_, __) => {
return navigateTo(ChatList({
selectedCharm: { charm: undefined },
Expand Down Expand Up @@ -97,88 +98,17 @@ const spawnNote = handler<void, void>((_, __) => {
}));
});

const toggle = handler<any, { value: Cell<boolean> }>((_, { value }) => {
value.set(!value.get());
});

const messagesToNotifications = lift<
{
messages: BuiltInLLMMessage[];
seen: Cell<number>;
notifications: Cell<{ text: string; timestamp: number }[]>;
}
>(({ messages, seen, notifications }) => {
if (messages.length > 0) {
if (seen.get() >= messages.length) {
// If messages length went backwards, reset seen counter
if (seen.get() > messages.length) {
seen.set(0);
} else {
return;
}
}

const latestMessage = messages[messages.length - 1];
if (latestMessage.role === "assistant") {
const contentText = typeof latestMessage.content === "string"
? latestMessage.content
: latestMessage.content.map((part) => {
if (part.type === "text") {
return part.text;
} else if (part.type === "tool-call") {
return `Tool call: ${part.toolName}`;
} else if (part.type === "tool-result") {
return part.output.type === "text"
? part.output.value
: JSON.stringify(part.output.value);
} else if (part.type === "image") {
return "[Image]";
}
return "";
}).join("");

notifications.push({
text: contentText,
timestamp: Date.now(),
});

seen.set(messages.length);
}
}
});

export default recipe<CharmsListInput, CharmsListOutput>(
"DefaultCharmList",
(_) => {
const allCharms = derive<MentionableCharm[], MentionableCharm[]>(
wish<MentionableCharm[]>("#allCharms", []),
wish<MentionableCharm[]>("#allCharms"),
(c) => c,
);
const index = BacklinksIndex({ allCharms });
const fabExpanded = cell(false);
const notifications = cell<{ text: string; timestamp: number }[]>([]);
const seen = cell<number>(0);

const omnibot = Chatbot({
messages: [],
tools: {
searchWeb: {
pattern: searchWeb,
},
readWebpage: {
pattern: readWebpage,
},
// Example of using patternTool with an existing recipe and extra params
calculator: patternTool(calculator, { base: 10 }),
},
});

messagesToNotifications({
messages: omnibot.messages,
seen: seen as unknown as Cell<number>,
notifications: notifications as unknown as Cell<
{ id: string; text: string; timestamp: number }[]
>,
const fab = OmniboxFAB({
mentionable: index.mentionable as unknown as Cell<MentionableCharm[]>,
});

return {
Expand All @@ -192,14 +122,20 @@ export default recipe<CharmsListInput, CharmsListOutput>(
preventDefault
onct-keybind={spawnChatList()}
/>

<ct-keybind
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capturing Cmd+O here prevents the browser’s standard “Open File” shortcut, so users lose access to a core browser feature.

Prompt for AI agents
Address the following comment on packages/patterns/default-app.tsx at line 125:

<comment>Capturing Cmd+O here prevents the browser’s standard “Open File” shortcut, so users lose access to a core browser feature.</comment>

<file context>
@@ -116,6 +122,18 @@ export default recipe&lt;CharmsListInput, CharmsListOutput&gt;(
             preventDefault
             onct-keybind={spawnChatList()}
           /&gt;
+          &lt;ct-keybind
+            code=&quot;KeyO&quot;
+            meta
</file context>
Fix with Cubic

code="Escape"
code="KeyO"
meta
preventDefault
onct-keybind={toggle({ value: fabExpanded })}
onct-keybind={toggleFab({ fabExpanded: fab.fabExpanded })}
/>
<ct-keybind
code="KeyO"
ctrl
preventDefault
onct-keybind={toggleFab({ fabExpanded: fab.fabExpanded })}
/>

<ct-toolbar slot="header">
<ct-toolbar slot="header" sticky>
<div slot="start">
<ct-button
onClick={spawnChatList()}
Expand Down Expand Up @@ -264,28 +200,8 @@ export default recipe<CharmsListInput, CharmsListOutput>(
</ct-vscroll>
</ct-screen>
),
sidebarUI: (
<div>
{/* TODO(bf): Remove once we fix types to not require ReactNode */}
{omnibot.ui.attachmentsAndTools as any}
{omnibot.ui.chatLog as any}
</div>
),
fabUI: (
<>
<ct-toast-stack
$notifications={notifications}
position="top-right"
auto-dismiss={5000}
max-toasts={5}
/>
{ifElse(
fabExpanded,
omnibot.ui.promptInput,
<ct-button onClick={toggle({ value: fabExpanded })}>✨</ct-button>,
)}
</>
),
sidebarUI: undefined,
fabUI: fab[UI],
};
},
);
Loading