From c5f8e0d0bf88190dcc7c106558f865c4badca46f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 07:57:00 +0000 Subject: [PATCH 01/12] feat(tui): add t1chat mode matching t3erchat UI - Added `t1chat.js` bin entry point which sets `T1CODE_CHAT_MODE=1` - Updated `apps/tui/src/ui.tsx` to read `isChatMode` - Implemented welcome screen mirroring `t3erchat` layout (categories, suggestions) - Added "Temp chat" toggle inside the composer toolbar for chat mode - Hidden git and diff features from the toolbar when in chat mode Co-authored-by: ahzs645 <31978381+ahzs645@users.noreply.github.com> --- apps/tui/bin/t1chat.js | 45 +++++++++++++++++ apps/tui/package.json | 3 +- apps/tui/src/ui.tsx | 107 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 151 insertions(+), 4 deletions(-) create mode 100755 apps/tui/bin/t1chat.js diff --git a/apps/tui/bin/t1chat.js b/apps/tui/bin/t1chat.js new file mode 100755 index 00000000..0b76b92a --- /dev/null +++ b/apps/tui/bin/t1chat.js @@ -0,0 +1,45 @@ +#!/usr/bin/env bun + +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const entryPath = fileURLToPath(new URL("../dist/index.mjs", import.meta.url)); + +function printError(error) { + process.stderr.write( + `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`, + ); +} + +process.env.T1CODE_CHAT_MODE = "1"; + +if (process.versions.bun === undefined) { + const bunBin = process.env.T1CODE_BUN_BIN?.trim() || "bun"; + const child = spawn(bunBin, [entryPath, ...process.argv.slice(2)], { + stdio: "inherit", + env: process.env, + }); + + child.once("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + process.exit(code ?? 1); + }); + + child.once("error", (error) => { + if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") { + printError("t1code requires Bun on your PATH to launch the TUI runtime."); + process.exit(1); + return; + } + printError(error); + process.exit(1); + }); +} else { + import("../dist/index.mjs").catch((error) => { + printError(error); + process.exit(1); + }); +} diff --git a/apps/tui/package.json b/apps/tui/package.json index f18b9008..c7ac48a9 100644 --- a/apps/tui/package.json +++ b/apps/tui/package.json @@ -14,7 +14,8 @@ }, "bin": { "t1": "./bin/t1code.js", - "t1code": "./bin/t1code.js" + "t1code": "./bin/t1code.js", + "t1chat": "./bin/t1chat.js" }, "files": [ "README.md", diff --git a/apps/tui/src/ui.tsx b/apps/tui/src/ui.tsx index bc516814..0e9e1037 100644 --- a/apps/tui/src/ui.tsx +++ b/apps/tui/src/ui.tsx @@ -2800,6 +2800,9 @@ export function App({ const [showScrollToBottom, setShowScrollToBottom] = useState(false); const previewAttachmentCacheRef = useRef>(new Map()); const composerDraftsByThreadIdRef = useRef>>({}); + + const isChatMode = process.env.T1CODE_CHAT_MODE === "1"; + const [tempChatMode, setTempChatMode] = useState(false); const updateAppSettings = useCallback((patch: Partial) => { setAppSettings((current) => normalizeAppSettings({ ...current, ...patch })); }, []); @@ -8120,7 +8123,7 @@ export function App({ marginRight={1} onPress={() => restoreDefaultSettings()} /> - ) : mainView === "keybindings" ? null : ( + ) : mainView === "keybindings" ? null : isChatMode ? null : ( <> - {!activeProject && !activeThread && !activeDraftThread ? ( + {isChatMode && (!activeThread || activeThread.messages.length === 0) ? ( + + + {tempChatMode ? ( + + + + + ) : ( + + )} + + + + {[ + { icon: "✨", label: "Create" }, + { icon: "📰", label: "Explore" }, + { icon: "💻", label: "Code" }, + { icon: "🎓", label: "Learn" }, + ].map((item) => ( + { + e.preventDefault(); + e.stopPropagation(); + syncComposerValueRefSoon(); + setComposer(`${item.label} `); + setTimeout(() => composerRef.current?.focus(), 0); + }} + style={{ + backgroundColor: PALETTE.surfaceAlt, + paddingLeft: 2, + paddingRight: 2, + paddingTop: 0, + paddingBottom: 0, + marginRight: 1, + marginBottom: 1, + flexDirection: "row", + alignItems: "center", + border: true, + borderStyle: "rounded", + borderColor: PALETTE.border, + }} + > + + + + ))} + + + + {[ + "How does AI work?", + "Are black holes real?", + 'How many Rs are in the word "strawberry"?', + "What is the meaning of life?", + ].map((q) => ( + { + e.preventDefault(); + e.stopPropagation(); + syncComposerValueRefSoon(); + setComposer(q); + setTimeout(() => composerRef.current?.focus(), 0); + }} + style={{ + border: ["top"], + borderColor: PALETTE.border, + paddingTop: 1, + paddingBottom: 1, + width: "100%", + }} + > + + + ))} + + + ) : !activeProject && !activeThread && !activeDraftThread ? ( + {isChatMode ? ( + + setTempChatMode((prev) => !prev)} + /> + + ) : null} {activePendingApproval ? ( <> @@ -9852,7 +9953,7 @@ export function App({ )} - {activeProjectId && isGitRepo ? ( + {activeProjectId && isGitRepo && !isChatMode ? ( Date: Tue, 7 Apr 2026 13:38:51 +0000 Subject: [PATCH 02/12] feat(tui): add t1chat mode matching t3erchat UI and colors - Added `t1chat.js` bin entry point which sets `T1CODE_CHAT_MODE=1` - Updated `apps/tui/src/ui.tsx` to read `isChatMode` - Implemented welcome screen mirroring `t3erchat` layout (categories, suggestions) - Added "Temp chat" toggle inside the composer toolbar for chat mode - Hidden git and diff features from the toolbar when in chat mode - Used exact hex color `#a23b67` for `t1chat` category buttons and UI elements to match `t3erchat` - Fixed React hook violation by extracting `ChatCategoryButton` component Co-authored-by: ahzs645 <31978381+ahzs645@users.noreply.github.com> --- apps/tui/src/ui.tsx | 66 ++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/apps/tui/src/ui.tsx b/apps/tui/src/ui.tsx index 0e9e1037..7051554c 100644 --- a/apps/tui/src/ui.tsx +++ b/apps/tui/src/ui.tsx @@ -1315,6 +1315,41 @@ function AttachmentPill({ ); } +function ChatCategoryButton(props: { + icon: string; + label: string; + onPress: () => void; +}) { + const [hoveredCategory, setHoveredCategory] = useState(false); + return ( + setHoveredCategory(true)} + onMouseOut={() => setHoveredCategory(false)} + onMouseDown={props.onPress} + style={{ + backgroundColor: hoveredCategory ? RGBA.fromHex("#a23b67") : PALETTE.surfaceAlt, + paddingLeft: 2, + paddingRight: 2, + paddingTop: 0, + paddingBottom: 0, + marginRight: 1, + marginBottom: 1, + flexDirection: "row", + alignItems: "center", + border: true, + borderStyle: "rounded", + borderColor: hoveredCategory ? RGBA.fromHex("#a23b67") : PALETTE.border, + }} + > + + + + ); +} + function PathSuggestionRow(props: { entry: ProjectEntry; active?: boolean; @@ -8952,8 +8987,8 @@ export function App({ {tempChatMode ? ( - - + + ) : ( @@ -8967,33 +9002,16 @@ export function App({ { icon: "💻", label: "Code" }, { icon: "🎓", label: "Learn" }, ].map((item) => ( - { - e.preventDefault(); - e.stopPropagation(); + icon={item.icon} + label={item.label} + onPress={() => { syncComposerValueRefSoon(); setComposer(`${item.label} `); setTimeout(() => composerRef.current?.focus(), 0); }} - style={{ - backgroundColor: PALETTE.surfaceAlt, - paddingLeft: 2, - paddingRight: 2, - paddingTop: 0, - paddingBottom: 0, - marginRight: 1, - marginBottom: 1, - flexDirection: "row", - alignItems: "center", - border: true, - borderStyle: "rounded", - borderColor: PALETTE.border, - }} - > - - - + /> ))} From 8e07d75f60e8e58fc84699daf02589d9eb544087 Mon Sep 17 00:00:00 2001 From: Ahmad Jalil Date: Tue, 7 Apr 2026 07:20:42 -0700 Subject: [PATCH 03/12] feat(tui): enhance chat mode support with responsive layout and theme updates --- apps/tui/src/responsiveLayout.ts | 3 +- apps/tui/src/theme.ts | 126 +++++++++++----------- apps/tui/src/ui.tsx | 179 +++++++++++++++++++++++++++++-- bun.lock | 10 +- scripts/dev-tui.ts | 1 + 5 files changed, 239 insertions(+), 80 deletions(-) diff --git a/apps/tui/src/responsiveLayout.ts b/apps/tui/src/responsiveLayout.ts index 7d6ad5b6..59d377cd 100644 --- a/apps/tui/src/responsiveLayout.ts +++ b/apps/tui/src/responsiveLayout.ts @@ -27,6 +27,7 @@ export type TuiResponsiveLayout = Readonly<{ export function resolveTuiResponsiveLayout(input: { viewportColumns: number; sidebarCollapsedPreference: boolean; + isChatMode?: boolean; }): TuiResponsiveLayout { const openSidebarMainPanelColumns = input.viewportColumns - TUI_SIDEBAR_WIDTH - 1; const showSidebarToggle = @@ -53,7 +54,7 @@ export function resolveTuiResponsiveLayout(input: { // should track sidebar visibility rather than the overall terminal width. showWindowDots: showSidebar, showSidebarAlphaBadge: showSidebar, - sidebarTitle: showSidebar ? "T1 Code" : "T1", + sidebarTitle: showSidebar ? (input.isChatMode ? "T1 Chat" : "T1 Code") : "T1", showHeaderProjectBadge: input.viewportColumns >= 144, showComposerModeLabels, showComposerModelLabel, diff --git a/apps/tui/src/theme.ts b/apps/tui/src/theme.ts index aa7676bc..781c4d4a 100644 --- a/apps/tui/src/theme.ts +++ b/apps/tui/src/theme.ts @@ -55,44 +55,44 @@ export interface ResolveTuiThemeOptions { } const DEFAULT_DARK_PALETTE = { - canvas: "#171717", - sidebar: "#151515", - main: "#171717", - surface: "#1b1b1b", - surfaceAlt: "#1f1f1f", - input: "#111111", - surfaceUser: "#202020", + canvas: "#21141e", + sidebar: "#1a0f18", + main: "#21141e", + surface: "#2a1825", + surfaceAlt: "#311e2c", + input: "#1a0f18", + surfaceUser: "#2a1825", surfacePlan: "#1f221c", surfaceWarn: "#262016", - surfaceInfo: "#1d2026", - footer: "#171717", - diff: "#1b1b1b", - popup: "#1c1c1c", + surfaceInfo: "#261a2e", + footer: "#21141e", + diff: "#2a1825", + popup: "#2a1825", scrim: "#00000099", - border: "#252525", - divider: "#2d2d2d", + border: "#3d2438", + divider: "#3d2438", control: "transparent", - controlHover: "#202020", - controlActive: "#292929", - controlActiveStrong: "#1e1e1e", - controlInset: "#141414", - controlInsetHover: "#1a1a1a", - composerPanel: "#1a1a1a", - composerBorder: "#2a3f95", - composerBorderMuted: "#313131", - composerSend: "#2f438e", - composerSendHover: "#3c57ba", + controlHover: "#311e2c", + controlActive: "#3d2438", + controlActiveStrong: "#2a1825", + controlInset: "#1a0f18", + controlInsetHover: "#21141e", + composerPanel: "#2a1825", + composerBorder: "#a3004c", + composerBorderMuted: "#3d2438", + composerSend: "#a3004c", + composerSendHover: "#e33f86", composerStop: "#dc2626", composerStopHover: "#ef4444", - accent: "#7c87ff", + accent: "#e33f86", cursor: "#d4d4d4", - selection: "#1f4f95", - selectionActive: "#2b61b0", - text: "#f5f5f5", - muted: "#a3a3a3", - subtle: "#737373", + selection: "#5c1a3e", + selectionActive: "#7a2450", + text: "#f9f8fb", + muted: "#b89eb5", + subtle: "#8a6b87", success: "#10b981", - info: "#3b82f6", + info: "#c074b2", warning: "#f59e0b", claude: "#d97757", macRed: "#ff5f57", @@ -105,23 +105,23 @@ export type TuiPalette = { [Key in keyof TuiPaletteShape]: TuiColor }; const DEFAULT_THEME_DETAILS = { attachmentPillTones: [ - { backgroundColor: "#1d2026", textColor: "#3b82f6" }, - { backgroundColor: "#241b2f", textColor: "#a78bfa" }, + { backgroundColor: "#2e1528", textColor: "#e33f86" }, + { backgroundColor: "#241b2f", textColor: "#c074b2" }, { backgroundColor: "#2a2417", textColor: "#facc15" }, { backgroundColor: "#2a1b1b", textColor: "#f87171" }, { backgroundColor: "#1c2721", textColor: "#34d399" }, { backgroundColor: "#272019", textColor: "#fb923c" }, ], codeBlock: { - background: "#101010", - language: "#8a8a8a", - copyIcon: "#9a9a9a", + background: "#1a0f18", + language: "#8a6b87", + copyIcon: "#b89eb5", }, status: { - awaitingInput: "#818cf8", - working: "#7dd3fc", + awaitingInput: "#e33f86", + working: "#c074b2", planReady: "#a78bfa", - pulse: "#3b82f6", + pulse: "#e33f86", }, diffViewer: { addedBg: "#173124", @@ -157,41 +157,41 @@ const DEFAULT_DARK_THEME: TuiTheme = { const DEFAULT_LIGHT_PALETTE: TuiPalette = { ...DEFAULT_DARK_PALETTE, - canvas: "#f5f5f5", - sidebar: "#eeeeee", - main: "#f7f7f7", + canvas: "#f2e1f4", + sidebar: "#ead0ef", + main: "#fdf7fd", surface: "#ffffff", - surfaceAlt: "#f1f1f1", + surfaceAlt: "#f5eaf6", input: "#ffffff", - surfaceUser: "#ececec", + surfaceUser: "#f0ddf2", surfacePlan: "#eef6ec", surfaceWarn: "#fff5e6", - surfaceInfo: "#eef4ff", - footer: "#f7f7f7", + surfaceInfo: "#f5eaff", + footer: "#fdf7fd", diff: "#fafafa", popup: "#ffffff", scrim: "#00000022", - border: "#dddddd", - divider: "#d8d8d8", - controlHover: "#ebebeb", - controlActive: "#e2e2e2", - controlActiveStrong: "#cdcdcd", - controlInset: "#e7e7e7", - controlInsetHover: "#dddddd", + border: "#efbdeb", + divider: "#e0b8dc", + controlHover: "#f0ddf2", + controlActive: "#e6cce9", + controlActiveStrong: "#d9b8dd", + controlInset: "#ead0ef", + controlInsetHover: "#e0c2e4", composerPanel: "#ffffff", - composerBorder: "#0891b2", - composerBorderMuted: "#d0d0d0", - composerSend: "#60a5fa", - composerSendHover: "#3b82f6", - accent: "#0891b2", + composerBorder: "#e33f86", + composerBorderMuted: "#e0b8dc", + composerSend: "#e33f86", + composerSendHover: "#ca0277", + accent: "#ca0277", cursor: "#a3a3a3", - selection: "#dbeafe", - selectionActive: "#bfdbfe", - text: "#171717", - muted: "#666666", - subtle: "#8a8a8a", + selection: "#f5d0e8", + selectionActive: "#f0b8dd", + text: "#501854", + muted: "#7a3f7e", + subtle: "#9a6b9e", success: "#059669", - info: "#2563eb", + info: "#8b3fa0", warning: "#d97706", claude: "#c96d4d", }; diff --git a/apps/tui/src/ui.tsx b/apps/tui/src/ui.tsx index 7051554c..b57564fe 100644 --- a/apps/tui/src/ui.tsx +++ b/apps/tui/src/ui.tsx @@ -3408,6 +3408,7 @@ export function App({ const responsiveLayout = resolveTuiResponsiveLayout({ viewportColumns: totalColumns, sidebarCollapsedPreference, + isChatMode, }); const showSidebarOverlay = !responsiveLayout.showSidebar && sidebarOverlayOpen; const showFullDiffView = mainView === "thread" && diffOpen; @@ -4091,7 +4092,7 @@ export function App({ const requestAppExit = useCallback(() => { setConfirmDialog({ - title: "Quit T1 Code?", + title: isChatMode ? "Quit T1 Chat?" : "Quit T1 Code?", body: "Press Ctrl-C again or Enter to quit. Press Escape to stay in the session.", confirmLabel: "Quit", escapeBehavior: "cancel", @@ -7760,7 +7761,7 @@ export function App({ style={{ width: responsiveLayout.showSidebar ? responsiveLayout.sidebarWidth : TUI_SIDEBAR_WIDTH, backgroundColor: sidebarBg, - border: ["right"], + border: isChatMode ? [] : ["right"], borderColor: PALETTE.divider, flexDirection: "column", ...(showSidebarOverlay @@ -7795,6 +7796,43 @@ export function App({ + {isChatMode ? ( + + { + if (activeProjectId) { + openDraftThread(activeProjectId); + } + }} + style={{ + backgroundColor: RGBA.fromHex("#a23b67"), + height: 1, + justifyContent: "center", + alignItems: "center", + paddingTop: 0, + paddingBottom: 0, + }} + > + + + + + + + + + + + ) : null} + - + />} - {projects.length === 0 ? ( + {!isChatMode && projects.length === 0 ? ( ) : null} - {sortedProjects.map((project) => { + {isChatMode ? (() => { + const allThreads = sortedProjects.flatMap((project) => { + const projectThreads = threadsByProject.get(project.id) ?? []; + return projectThreads.map((thread) => ({ ...thread, projectId: project.id })); + }); + allThreads.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); + + const now = new Date(); + const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const yesterdayStart = new Date(todayStart.getTime() - 86400000); + const weekStart = new Date(todayStart.getTime() - 7 * 86400000); + const monthStart = new Date(todayStart.getTime() - 30 * 86400000); + + type TimeGroup = { label: string; threads: typeof allThreads }; + const groups: TimeGroup[] = [ + { label: "Today", threads: [] }, + { label: "Yesterday", threads: [] }, + { label: "Last 7 Days", threads: [] }, + { label: "Last 30 Days", threads: [] }, + { label: "Older", threads: [] }, + ]; + + for (const thread of allThreads) { + const date = new Date(thread.updatedAt); + if (date >= todayStart) groups[0]!.threads.push(thread); + else if (date >= yesterdayStart) groups[1]!.threads.push(thread); + else if (date >= weekStart) groups[2]!.threads.push(thread); + else if (date >= monthStart) groups[3]!.threads.push(thread); + else groups[4]!.threads.push(thread); + } + + return groups.filter((g) => g.threads.length > 0).map((group) => ( + + + {group.threads.map((thread) => { + const isActive = thread.id === activeThreadId; + const isSelected = selectedThreadIds.has(thread.id); + const status = threadStatus(thread, { + forceUnread: locallyUnreadThreadIds.has(thread.id), + locallyVisitedAt: locallyVisitedThreads[thread.id], + }); + return ( + { + closeSidebarContextMenu(); + handleThreadClick( + event, + thread.projectId, + thread.id, + allThreads.map((t) => t.id), + ); + }} + onSecondaryPress={(event) => { + openThreadContextMenu(thread.projectId, thread.id, event); + }} + > + + {status ? ( + + ) : null} + + + + + + ); + })} + + )); + })() : null} + + {!isChatMode ? sortedProjects.map((project) => { const projectThreads = threadsByProject.get(project.id) ?? []; const orderedProjectThreadIds = projectThreads.map((thread) => thread.id); const isProjectExpanded = expandedProjectIds.has(project.id); @@ -8034,7 +8181,7 @@ export function App({ ) : null} ); - })} + }) : null} + {isChatMode && responsiveLayout.showSidebar ? ( + + + + + + ) : null} - + ) : ( - + )} diff --git a/bun.lock b/bun.lock index 8ba9b2ce..94ed3dab 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ }, "apps/desktop": { "name": "@t3tools/desktop", - "version": "0.0.16", + "version": "0.0.21", "dependencies": { "effect": "catalog:", "electron": "40.6.0", @@ -43,7 +43,7 @@ }, "apps/server": { "name": "t3", - "version": "0.0.16", + "version": "0.0.21", "bin": { "t3": "./dist/index.mjs", }, @@ -73,7 +73,7 @@ }, "apps/tui": { "name": "@maria_rcks/t1code", - "version": "0.0.19", + "version": "0.0.21", "bin": { "t1code": "./bin/t1code.js", }, @@ -107,7 +107,7 @@ }, "apps/web": { "name": "@t3tools/web", - "version": "0.0.16", + "version": "0.0.21", "dependencies": { "@base-ui/react": "^1.2.0", "@dnd-kit/core": "^6.3.1", @@ -173,7 +173,7 @@ }, "packages/contracts": { "name": "@t3tools/contracts", - "version": "0.0.16", + "version": "0.0.21", "dependencies": { "effect": "catalog:", }, diff --git a/scripts/dev-tui.ts b/scripts/dev-tui.ts index c7bbedbf..1722252c 100644 --- a/scripts/dev-tui.ts +++ b/scripts/dev-tui.ts @@ -231,6 +231,7 @@ const tui = spawn("bun", ["--silent", "--watch", "run", TUI_ENTRY], { T1CODE_PORT: String(port), T1CODE_AUTH_TOKEN: authToken, T1CODE_TUI_ATTACH_ONLY: "1", + ...(process.env.T1CODE_CHAT_MODE ? { T1CODE_CHAT_MODE: process.env.T1CODE_CHAT_MODE } : {}), }, stdio: "inherit", }); From 355bd1b2e191ed94982d111759d3d18b1c1efd24 Mon Sep 17 00:00:00 2001 From: Ahmad Jalil Date: Tue, 7 Apr 2026 07:46:09 -0700 Subject: [PATCH 04/12] feat(tui): implement sidebar search functionality for threads --- apps/tui/src/ui.tsx | 138 +++++++++++++++++++++++++++++++++----------- 1 file changed, 103 insertions(+), 35 deletions(-) diff --git a/apps/tui/src/ui.tsx b/apps/tui/src/ui.tsx index b57564fe..d36c68ac 100644 --- a/apps/tui/src/ui.tsx +++ b/apps/tui/src/ui.tsx @@ -2838,6 +2838,7 @@ export function App({ const isChatMode = process.env.T1CODE_CHAT_MODE === "1"; const [tempChatMode, setTempChatMode] = useState(false); + const [sidebarSearchQuery, setSidebarSearchQuery] = useState(""); const updateAppSettings = useCallback((patch: Partial) => { setAppSettings((current) => normalizeAppSettings({ ...current, ...patch })); }, []); @@ -7816,19 +7817,39 @@ export function App({ setFocusArea("projects")} style={{ height: 1, flexDirection: "row", alignItems: "center", marginTop: 1, paddingBottom: 0, + position: "relative", }} > - - + + {sidebarSearchQuery ? null : ( + + )} + setSidebarSearchQuery(value)} + style={{ + flexGrow: 1, + backgroundColor: sidebarBg, + textColor: PALETTE.muted, + focusedTextColor: PALETTE.muted, + focusedBackgroundColor: sidebarBg, + }} + /> - - + + ) : null} @@ -7883,6 +7904,10 @@ export function App({ return projectThreads.map((thread) => ({ ...thread, projectId: project.id })); }); allThreads.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); + const searchLower = sidebarSearchQuery.toLowerCase().trim(); + const filteredThreads = searchLower + ? allThreads.filter((t) => t.title.toLowerCase().includes(searchLower)) + : allThreads; const now = new Date(); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()); @@ -7899,7 +7924,7 @@ export function App({ { label: "Older", threads: [] }, ]; - for (const thread of allThreads) { + for (const thread of filteredThreads) { const date = new Date(thread.updatedAt); if (date >= todayStart) groups[0]!.threads.push(thread); else if (date >= yesterdayStart) groups[1]!.threads.push(thread); @@ -8248,29 +8273,54 @@ export function App({ }} > {isChatMode && responsiveLayout.showSidebar ? ( - - - - - + <> + {/* Row 1: top bar, full sidebar bg connects to sidebar */} + + {/* Row 2: icons row, main bg on left, sidebar bg with icons on right */} + + + + setTempChatMode((prev) => !prev)} + /> + { + openMainView("settings"); + }} + /> + + + ) : null} + {isChatMode && responsiveLayout.showSidebar ? null : ( + {isChatMode && responsiveLayout.showSidebar ? null : ( ) : null} + )} + {isChatMode && responsiveLayout.showSidebar ? null : ( restoreDefaultSettings()} /> - ) : mainView === "keybindings" ? null : isChatMode ? null : ( + ) : mainView === "keybindings" ? null : isChatMode ? ( + <> + setTempChatMode((prev) => !prev)} + /> + { + openMainView("settings"); + }} + /> + + ) : ( <> )} + )} + )} ) : ( - + )} - {isChatMode ? ( - - setTempChatMode((prev) => !prev)} - /> - - ) : null} + {null} {activePendingApproval ? ( <> @@ -10045,8 +10113,8 @@ export function App({ /> ) : null} - {responsiveLayout.showComposerDividers ? : null} - : null} + {!isChatMode ? - {responsiveLayout.showComposerDividers ? : null} - : null} + {!isChatMode && responsiveLayout.showComposerDividers ? : null} + {!isChatMode ? + /> : null} {activePendingProgress ? ( <> From ca1721fc279ab7f1b9f4c5677b6bdb6458f44ea2 Mon Sep 17 00:00:00 2001 From: Ahmad Jalil Date: Tue, 7 Apr 2026 07:51:52 -0700 Subject: [PATCH 05/12] feat(tui): add bottom padding and sidebar background to match icon area --- apps/tui/src/ui.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/tui/src/ui.tsx b/apps/tui/src/ui.tsx index d36c68ac..6210eb06 100644 --- a/apps/tui/src/ui.tsx +++ b/apps/tui/src/ui.tsx @@ -7848,8 +7848,8 @@ export function App({ }} /> - - + + ) : null} @@ -7861,6 +7861,7 @@ export function App({ ...themedScrollboxStyle(sidebarBg), paddingLeft: 1, paddingRight: 1, + paddingTop: isChatMode ? 1 : 0, }} > {isChatMode ? null : + {/* Row 3: bottom padding, sidebar bg only on right to match icon area */} + + + + ) : null} {isChatMode && responsiveLayout.showSidebar ? null : ( From 69f44599c51389346a982803aa83132ab506a53c Mon Sep 17 00:00:00 2001 From: Ahmad Jalil Date: Tue, 7 Apr 2026 07:55:45 -0700 Subject: [PATCH 06/12] feat(tui): update composer placeholder and adjust layout for chat mode --- apps/tui/src/ui.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/tui/src/ui.tsx b/apps/tui/src/ui.tsx index 6210eb06..cd52e0d5 100644 --- a/apps/tui/src/ui.tsx +++ b/apps/tui/src/ui.tsx @@ -7147,7 +7147,7 @@ export function App({ ? activeDraftThread ? "Start a new thread with a prompt" : "Ask for follow-up changes or attach images" - : COMPOSER_PLACEHOLDER; + : isChatMode ? "Type your message here..." : COMPOSER_PLACEHOLDER; const composerPathTrigger = detectTrailingComposerPathTrigger(composer); const showPathSuggestions = composerIsFocused && @@ -7785,8 +7785,8 @@ export function App({ paddingRight: 2, }} > - - {responsiveLayout.showWindowDots ? : null} + + {responsiveLayout.showWindowDots && !isChatMode ? : null} Date: Tue, 7 Apr 2026 08:02:51 -0700 Subject: [PATCH 07/12] feat(tui): update chat UI elements for improved layout and styling --- apps/tui/src/ui.tsx | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/apps/tui/src/ui.tsx b/apps/tui/src/ui.tsx index cd52e0d5..6589fbbb 100644 --- a/apps/tui/src/ui.tsx +++ b/apps/tui/src/ui.tsx @@ -1341,7 +1341,7 @@ function ChatCategoryButton(props: { borderColor: hoveredCategory ? RGBA.fromHex("#a23b67") : PALETTE.border, }} > - + @@ -9220,9 +9220,11 @@ export function App({ style={{ flexGrow: 1, flexDirection: "column", - justifyContent: "center", - alignItems: "center", - padding: 2, + justifyContent: "flex-start", + alignItems: "flex-start", + paddingLeft: 2, + paddingRight: 2, + paddingTop: 4, }} > @@ -9232,16 +9234,16 @@ export function App({ ) : ( - + )} - + {[ - { icon: "✨", label: "Create" }, - { icon: "📰", label: "Explore" }, - { icon: "💻", label: "Code" }, - { icon: "🎓", label: "Learn" }, + { icon: "󰛕", label: "Create" }, + { icon: "󰎕", label: "Explore" }, + { icon: "󰅪", label: "Code" }, + { icon: "󰑴", label: "Learn" }, ].map((item) => ( - + {[ "How does AI work?", "Are black holes real?", 'How many Rs are in the word "strawberry"?', "What is the meaning of life?", - ].map((q) => ( + ].map((q, i) => ( { @@ -9273,10 +9275,12 @@ export function App({ setTimeout(() => composerRef.current?.focus(), 0); }} style={{ - border: ["top"], - borderColor: PALETTE.border, - paddingTop: 1, - paddingBottom: 1, + border: i > 0 ? ["top"] : [], + borderColor: PALETTE.divider, + paddingTop: 0, + paddingBottom: 0, + height: 2, + alignItems: "center", width: "100%", }} > @@ -9716,7 +9720,7 @@ export function App({ position: "relative", zIndex: 20, backgroundColor: PALETTE.composerPanel, - border: true, + border: isChatMode ? ["top", "left", "right"] : true, borderStyle: "rounded", borderColor: activePendingProgress ? focusArea === "composer" @@ -9726,7 +9730,7 @@ export function App({ ? PALETTE.composerBorder : PALETTE.composerBorderMuted, paddingTop: activePendingProgress ? 0 : 1, - paddingBottom: isChatMode ? 0 : 1, + paddingBottom: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "column", @@ -9750,7 +9754,7 @@ export function App({ Date: Tue, 7 Apr 2026 08:26:19 -0700 Subject: [PATCH 08/12] feat(tui): add profile management for chat mode with creation and reordering functionality --- README.md | 43 +++++++++- apps/tui/src/profiles.ts | 72 ++++++++++++++++ apps/tui/src/ui.tsx | 179 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 287 insertions(+), 7 deletions(-) create mode 100644 apps/tui/src/profiles.ts diff --git a/README.md b/README.md index 1bb6c21c..29c48a02 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![License](https://img.shields.io/badge/license-MIT-111111?style=flat-square)](./LICENSE) [![npm](https://img.shields.io/npm/v/%40maria__rcks%2Ft1code?color=111111&label=npm&style=flat-square)](https://www.npmjs.com/package/@maria_rcks/t1code) -[![GitHub](https://img.shields.io/badge/github-maria--rcks%2Ft1code-111111?style=flat-square&logo=github)](https://github.com/maria-rcks/t1code) +[![GitHub](https://img.shields.io/badge/github-ahzs645%2Ft1chat-111111?style=flat-square&logo=github)](https://github.com/ahzs645/t1chat) t1code terminal UI screenshot @@ -12,6 +12,8 @@ _T3Code, but in your terminal._ +## t1code (code mode) + Run instantly: ```bash @@ -27,10 +29,47 @@ bun add -g @maria_rcks/t1code Develop from source: ```bash -git clone https://github.com/maria-rcks/t1code.git +git clone https://github.com/ahzs645/t1chat.git cd t1code bun install bun dev:tui ``` +## t1chat (chat mode) + +t1chat is a chat-focused mode that transforms the TUI into a conversational interface inspired by [T3 Chat](https://t3.chat). It features a pink/magenta/lavender theme, a flat thread list grouped by time, and a streamlined UI without code-specific tools. + +### What changes in chat mode + +- Sidebar shows a flat thread list grouped by time (Today, Yesterday, Last 7 Days, etc.) instead of nested projects +- "New Chat" button and thread search in the sidebar +- Title shows "T1 Chat" instead of "T1 Code" +- Git tools, diff viewer, Chat/Plan toggle, and Full access button are hidden +- Settings and temp chat toggle in the top-right corner +- Composer placeholder says "Type your message here..." +- Pink/magenta/lavender color scheme matching T3 Chat + +### Run chat mode + +If installed globally: + +```bash +t1chat +``` + +Run instantly: + +```bash +bunx @maria_rcks/t1code t1chat +``` + +Develop from source: + +```bash +git clone https://github.com/ahzs645/t1chat.git +cd t1code +bun install +T1CODE_CHAT_MODE=1 bun dev:tui +``` + Based on T3 Code by [@t3dotgg](https://github.com/t3dotgg) and [@juliusmarminge](https://github.com/juliusmarminge). diff --git a/apps/tui/src/profiles.ts b/apps/tui/src/profiles.ts new file mode 100644 index 00000000..1fbe56d8 --- /dev/null +++ b/apps/tui/src/profiles.ts @@ -0,0 +1,72 @@ +/** + * Profile management for t1chat mode. + * + * Profiles let users organize conversations under different personas. + * Each profile has a name, icon, and unique ID. Threads can be + * associated with a profile so switching profiles filters the sidebar. + */ + +export interface Profile { + id: string; + name: string; + icon: string; +} + +/** Nerd Font icons available for profile selection. */ +export const PROFILE_ICON_OPTIONS: { icon: string; label: string }[] = [ + { icon: "󰭹", label: "Chat" }, + { icon: "󰫢", label: "Star" }, + { icon: "󰃀", label: "Bookmark" }, + { icon: "󰋑", label: "Heart" }, + { icon: "󰈻", label: "Flag" }, + { icon: "󱐋", label: "Lightning" }, + { icon: "󰐊", label: "Play" }, + { icon: "󰛕", label: "Sparkles" }, + { icon: "󰂞", label: "Bell" }, + { icon: "󰛨", label: "Bulb" }, + { icon: "󰋜", label: "Home" }, + { icon: "󰉋", label: "Folder" }, + { icon: "󰃭", label: "Calendar" }, + { icon: "󰇮", label: "Mail" }, + { icon: "󰈙", label: "File" }, + { icon: "󰂺", label: "Book" }, + { icon: "󰊗", label: "Briefcase" }, + { icon: "󰆼", label: "Database" }, + { icon: "󰳗", label: "Cube" }, + { icon: "󰕮", label: "Music" }, + { icon: "󰄀", label: "Camera" }, + { icon: "󰈈", label: "Eye" }, + { icon: "󰟃", label: "Globe" }, + { icon: "󰑴", label: "Graduate" }, +]; + +export const DEFAULT_PROFILE: Profile = { + id: "default", + name: "Default", + icon: "󰭹", +}; + +export function createProfile(name: string, icon: string): Profile { + const slug = name + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, ""); + return { + id: `${slug}-${Date.now()}`, + name, + icon, + }; +} + +export function reorderProfiles( + profiles: Profile[], + fromIndex: number, + toIndex: number, +): Profile[] { + const result = [...profiles]; + const [moved] = result.splice(fromIndex, 1); + if (moved) { + result.splice(toIndex, 0, moved); + } + return result; +} diff --git a/apps/tui/src/ui.tsx b/apps/tui/src/ui.tsx index 6589fbbb..f14f2bef 100644 --- a/apps/tui/src/ui.tsx +++ b/apps/tui/src/ui.tsx @@ -194,6 +194,7 @@ import { shouldClearPendingCreatedThread, } from "./threadSelection"; import { resolveWorkEntryIcon } from "./workEntryIcons"; +import { createProfile, PROFILE_ICON_OPTIONS } from "./profiles"; type FocusArea = | "projects" @@ -2839,6 +2840,16 @@ export function App({ const isChatMode = process.env.T1CODE_CHAT_MODE === "1"; const [tempChatMode, setTempChatMode] = useState(false); const [sidebarSearchQuery, setSidebarSearchQuery] = useState(""); + const [chatProfiles, setChatProfiles] = useState([ + { id: "default", name: "Default", icon: "󰭹" }, + ]); + const [activeProfileId, setActiveProfileId] = useState("default"); + const [threadProfileMap, setThreadProfileMap] = useState>({}); + const [showProfileCreate, setShowProfileCreate] = useState(false); + const [newProfileName, setNewProfileName] = useState(""); + const [newProfileIconIndex, setNewProfileIconIndex] = useState(0); + const [profileNameFocused, setProfileNameFocused] = useState(false); + const [profileIconFocused, setProfileIconFocused] = useState(false); const updateAppSettings = useCallback((patch: Partial) => { setAppSettings((current) => normalizeAppSettings({ ...current, ...patch })); }, []); @@ -5784,6 +5795,9 @@ export function App({ setExpandedProjectIds((current) => ensureProjectExpanded(current, projectId)); setFocusArea("composer"); setStatus("New thread"); + if (isChatMode) { + setThreadProfileMap((current) => ({ ...current, [existingDraft.id]: activeProfileId })); + } setTimeout(() => { composerRef.current?.focus(); }, 0); @@ -7905,10 +7919,15 @@ export function App({ return projectThreads.map((thread) => ({ ...thread, projectId: project.id })); }); allThreads.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); + const profileThreads = allThreads.filter((t) => { + const threadProfile = threadProfileMap[t.id]; + if (!threadProfile) return activeProfileId === "default"; + return threadProfile === activeProfileId; + }); const searchLower = sidebarSearchQuery.toLowerCase().trim(); const filteredThreads = searchLower - ? allThreads.filter((t) => t.title.toLowerCase().includes(searchLower)) - : allThreads; + ? profileThreads.filter((t) => t.title.toLowerCase().includes(searchLower)) + : profileThreads; const now = new Date(); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()); @@ -8210,6 +8229,154 @@ export function App({ }) : null} + {isChatMode ? ( + + {showProfileCreate ? ( + + + + + { + setNewProfileIconIndex((i) => (i > 0 ? i - 1 : PROFILE_ICON_OPTIONS.length - 1)); + setProfileIconFocused(true); + setProfileNameFocused(false); + }} + style={{ + border: true, + borderStyle: "rounded", + borderColor: profileIconFocused ? RGBA.fromHex("#db2777") : PALETTE.border, + paddingLeft: 1, + paddingRight: 1, + marginRight: 1, + backgroundColor: RGBA.fromHex("#eaa7cb"), + justifyContent: "center", + alignItems: "center", + }} + > + + + { setProfileNameFocused(true); setProfileIconFocused(false); }} + style={{ flexGrow: 1, border: true, borderStyle: "rounded", borderColor: profileNameFocused ? RGBA.fromHex("#db2777") : PALETTE.border, backgroundColor: RGBA.fromHex("#eaa7cb"), justifyContent: "center", paddingLeft: 1 }} + > + { + setNewProfileName(value.slice(0, 50)); + setProfileNameFocused(true); + }} + style={{ + flexGrow: 1, + backgroundColor: RGBA.fromHex("#eaa7cb"), + textColor: PALETTE.text, + focusedTextColor: PALETTE.text, + focusedBackgroundColor: RGBA.fromHex("#eaa7cb"), + }} + /> + + + { + if (newProfileName.trim()) { + const icon = PROFILE_ICON_OPTIONS[newProfileIconIndex]?.icon ?? "󰭹"; + const profile = createProfile(newProfileName.trim(), icon); + setChatProfiles((prev) => [...prev, profile]); + setActiveProfileId(profile.id); + setNewProfileName(""); + setNewProfileIconIndex(0); + setProfileNameFocused(false); + setProfileIconFocused(false); + setShowProfileCreate(false); + } + }} + style={{ + backgroundColor: RGBA.fromHex(newProfileName.trim() ? "#e33f86" : "#f19dc5"), + height: 1, + justifyContent: "center", + alignItems: "center", + }} + > + + + + ) : null} + + + {chatProfiles.map((profile) => ( + setActiveProfileId(profile.id)} + style={{ + marginRight: 1, + width: 3, + height: 3, + justifyContent: "center", + alignItems: "center", + backgroundColor: + profile.id === activeProfileId ? PALETTE.controlActive : "transparent", + }} + > + + + ))} + + { + setShowProfileCreate((v) => !v); + if (showProfileCreate) { + setNewProfileName(""); + setNewProfileIconIndex(0); + setProfileNameFocused(false); + } + }} + style={{ + width: 3, + height: 3, + justifyContent: "center", + alignItems: "center", + }} + > + + + + + ) : ( + )} ) : null} @@ -9238,7 +9406,7 @@ export function App({ )} - + {[ { icon: "󰛕", label: "Create" }, { icon: "󰎕", label: "Explore" }, @@ -9279,8 +9447,9 @@ export function App({ borderColor: PALETTE.divider, paddingTop: 0, paddingBottom: 0, - height: 2, - alignItems: "center", + height: i === 0 ? 1 : 2, + alignItems: "flex-start", + justifyContent: "center", width: "100%", }} > From 2bbb929eaf9fdb2f6a216eda4e091c9a9ce4ea3f Mon Sep 17 00:00:00 2001 From: Ahmad Jalil Date: Tue, 7 Apr 2026 08:36:30 -0700 Subject: [PATCH 09/12] feat(tui): add boring theme support and chat settings overlay --- apps/tui/src/theme.ts | 84 +++++++++++++++++++++++++++++- apps/tui/src/ui.tsx | 117 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 190 insertions(+), 11 deletions(-) diff --git a/apps/tui/src/theme.ts b/apps/tui/src/theme.ts index 781c4d4a..43971d07 100644 --- a/apps/tui/src/theme.ts +++ b/apps/tui/src/theme.ts @@ -16,12 +16,14 @@ export interface TerminalColors { export type TuiColor = string; export const TERMINAL_MATCH_THEME_ID = "terminal-match" as const; -export const TUI_THEME_IDS = ["default", TERMINAL_MATCH_THEME_ID] as const; +export const BORING_THEME_ID = "boring" as const; +export const TUI_THEME_IDS = ["default", TERMINAL_MATCH_THEME_ID, BORING_THEME_ID] as const; export type TuiThemeId = (typeof TUI_THEME_IDS)[number]; export const DEFAULT_TUI_THEME_ID = "default" as const; export const TUI_THEME_LABELS: Record = { default: "Default", [TERMINAL_MATCH_THEME_ID]: "Terminal Match", + [BORING_THEME_ID]: "Boring", }; export type TuiThemeMode = "light" | "dark"; @@ -207,6 +209,84 @@ const DEFAULT_LIGHT_THEME: TuiTheme = { }, }; +const BORING_DARK_PALETTE: TuiPalette = { + ...DEFAULT_DARK_PALETTE, + canvas: "#151515", + sidebar: "#1a1a1a", + main: "#151515", + surface: "#1e1e1e", + surfaceAlt: "#222222", + input: "#1a1a1a", + surfaceUser: "#1e1e1e", + footer: "#151515", + popup: "#1e1e1e", + border: "#282828", + divider: "#282828", + controlHover: "#252525", + controlActive: "#2a2a2a", + controlActiveStrong: "#222222", + controlInset: "#1a1a1a", + controlInsetHover: "#202020", + composerPanel: "#1e1e1e", + composerBorder: "#763750", + composerBorderMuted: "#333333", + composerSend: "#763750", + composerSendHover: "#ad5273", + accent: "#ad5273", + selection: "#3a2030", + selectionActive: "#4a2840", + text: "#e6e6e6", + muted: "#b0b0b0", + subtle: "#707070", + info: "#888888", +}; + +const BORING_LIGHT_PALETTE: TuiPalette = { + ...DEFAULT_LIGHT_PALETTE, + canvas: "#ebebeb", + sidebar: "#e0e0e0", + main: "#f0f0f0", + surface: "#ffffff", + surfaceAlt: "#e8e8e8", + input: "#ffffff", + surfaceUser: "#e0e0e0", + footer: "#f0f0f0", + popup: "#ffffff", + border: "#d4d4d4", + divider: "#d0d0d0", + controlHover: "#e0e0e0", + controlActive: "#d4d4d4", + controlActiveStrong: "#c8c8c8", + controlInset: "#e0e0e0", + controlInsetHover: "#d8d8d8", + composerPanel: "#ffffff", + composerBorder: "#ad5273", + composerBorderMuted: "#c9c9c9", + composerSend: "#ad5273", + composerSendHover: "#8a3f5c", + accent: "#ad5273", + selection: "#e8d0dd", + selectionActive: "#ddbece", + text: "#171717", + muted: "#616161", + subtle: "#8a8a8a", + info: "#666666", +}; + +const BORING_DARK_THEME: TuiTheme = { + ...DEFAULT_DARK_THEME, + id: BORING_THEME_ID, + mode: "dark", + palette: BORING_DARK_PALETTE, +}; + +const BORING_LIGHT_THEME: TuiTheme = { + ...DEFAULT_LIGHT_THEME, + id: BORING_THEME_ID, + mode: "light", + palette: BORING_LIGHT_PALETTE, +}; + export const DEFAULT_TUI_THEME = DEFAULT_DARK_THEME; function defaultThemeForMode(mode: TuiThemeMode): TuiTheme { @@ -714,6 +794,8 @@ export function resolveTuiThemeMode( const THEME_CACHE = new Map([ [`${DEFAULT_TUI_THEME_ID}:dark`, DEFAULT_DARK_THEME], [`${DEFAULT_TUI_THEME_ID}:light`, DEFAULT_LIGHT_THEME], + [`${BORING_THEME_ID}:dark`, BORING_DARK_THEME], + [`${BORING_THEME_ID}:light`, BORING_LIGHT_THEME], ]); export function resolveTuiTheme( diff --git a/apps/tui/src/ui.tsx b/apps/tui/src/ui.tsx index f14f2bef..5173ab2c 100644 --- a/apps/tui/src/ui.tsx +++ b/apps/tui/src/ui.tsx @@ -195,6 +195,7 @@ import { } from "./threadSelection"; import { resolveWorkEntryIcon } from "./workEntryIcons"; import { createProfile, PROFILE_ICON_OPTIONS } from "./profiles"; +import { BORING_THEME_ID } from "./theme"; type FocusArea = | "projects" @@ -214,7 +215,8 @@ type OverlayMenu = | "sidebar-sort" | "git-actions" | "composer-env" - | "composer-branch"; + | "composer-branch" + | "chat-settings"; type SettingsSelectKind = | "theme" | "theme-preset" @@ -8247,7 +8249,7 @@ export function App({ bottom: 4, left: 0, right: 0, - backgroundColor: RGBA.fromHex("#eaa7cb"), + backgroundColor: PALETTE.surfaceAlt, border: ["top"], borderColor: PALETTE.divider, flexDirection: "column", @@ -8267,11 +8269,11 @@ export function App({ style={{ border: true, borderStyle: "rounded", - borderColor: profileIconFocused ? RGBA.fromHex("#db2777") : PALETTE.border, + borderColor: profileIconFocused ? PALETTE.composerBorder : PALETTE.border, paddingLeft: 1, paddingRight: 1, marginRight: 1, - backgroundColor: RGBA.fromHex("#eaa7cb"), + backgroundColor: PALETTE.surfaceAlt, justifyContent: "center", alignItems: "center", }} @@ -8283,7 +8285,7 @@ export function App({ { setProfileNameFocused(true); setProfileIconFocused(false); }} - style={{ flexGrow: 1, border: true, borderStyle: "rounded", borderColor: profileNameFocused ? RGBA.fromHex("#db2777") : PALETTE.border, backgroundColor: RGBA.fromHex("#eaa7cb"), justifyContent: "center", paddingLeft: 1 }} + style={{ flexGrow: 1, border: true, borderStyle: "rounded", borderColor: profileNameFocused ? PALETTE.composerBorder : PALETTE.border, backgroundColor: PALETTE.surfaceAlt, justifyContent: "center", paddingLeft: 1 }} > @@ -8318,7 +8320,7 @@ export function App({ } }} style={{ - backgroundColor: RGBA.fromHex(newProfileName.trim() ? "#e33f86" : "#f19dc5"), + backgroundColor: newProfileName.trim() ? PALETTE.composerSend : PALETTE.controlActive, height: 1, justifyContent: "center", alignItems: "center", @@ -8465,9 +8467,10 @@ export function App({ chrome="bare" width={4} justifyContent="flex-end" - iconColor={PALETTE.muted} + iconColor={overlayMenu === "chat-settings" ? PALETTE.text : PALETTE.muted} + active={overlayMenu === "chat-settings"} onPress={() => { - openMainView("settings"); + setOverlayMenu((current) => current === "chat-settings" ? null : "chat-settings"); }} /> @@ -11027,6 +11030,100 @@ export function App({ ) : null} + {overlayMenu === "chat-settings" ? ( + { + event.preventDefault(); + event.stopPropagation?.(); + }} + > + + + { + updateAppSettings({ theme: "light" }); + setOverlayMenu(null); + }} + style={{ + paddingLeft: 1, + paddingRight: 1, + backgroundColor: appSettings.theme === "light" ? PALETTE.controlActive : "transparent", + }} + > + + + { + updateAppSettings({ theme: "system" }); + setOverlayMenu(null); + }} + style={{ + paddingLeft: 1, + paddingRight: 1, + backgroundColor: appSettings.theme === "system" || !appSettings.theme ? PALETTE.controlActive : "transparent", + }} + > + + + { + updateAppSettings({ theme: "dark" }); + setOverlayMenu(null); + }} + style={{ + paddingLeft: 1, + paddingRight: 1, + backgroundColor: appSettings.theme === "dark" ? PALETTE.controlActive : "transparent", + }} + > + + + + { + setTuiThemeId((current) => current === BORING_THEME_ID ? "default" : BORING_THEME_ID); + }} + style={{ height: 1, flexDirection: "row", alignItems: "center" }} + > + + + + + + + + { + openMainView("settings"); + setOverlayMenu(null); + }} + style={{ height: 1, flexDirection: "row", alignItems: "center" }} + > + + + + + ) : null} + {confirmDialog ? ( Date: Tue, 7 Apr 2026 08:40:51 -0700 Subject: [PATCH 10/12] Refactor code structure for improved readability and maintainability --- README.md | 4 ++++ apps/tui/src/ui.tsx | 13 ++++++++++++- assets/repo/t1chat-preview.png | Bin 0 -> 49956 bytes 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 assets/repo/t1chat-preview.png diff --git a/README.md b/README.md index 29c48a02..d3559803 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,10 @@ bun dev:tui ## t1chat (chat mode) +
+t1chat terminal UI screenshot +
+ t1chat is a chat-focused mode that transforms the TUI into a conversational interface inspired by [T3 Chat](https://t3.chat). It features a pink/magenta/lavender theme, a flat thread list grouped by time, and a streamlined UI without code-specific tools. ### What changes in chat mode diff --git a/apps/tui/src/ui.tsx b/apps/tui/src/ui.tsx index 5173ab2c..619334f7 100644 --- a/apps/tui/src/ui.tsx +++ b/apps/tui/src/ui.tsx @@ -2839,7 +2839,7 @@ export function App({ const previewAttachmentCacheRef = useRef>(new Map()); const composerDraftsByThreadIdRef = useRef>>({}); - const isChatMode = process.env.T1CODE_CHAT_MODE === "1"; + const [isChatMode, setIsChatMode] = useState(process.env.T1CODE_CHAT_MODE === "1"); const [tempChatMode, setTempChatMode] = useState(false); const [sidebarSearchQuery, setSidebarSearchQuery] = useState(""); const [chatProfiles, setChatProfiles] = useState([ @@ -8636,6 +8636,17 @@ export function App({ {mainView === "settings" ? ( <> + setIsChatMode((v) => !v)} + /> + } + /> FJ?f_6`OH&I<+xRuLHy zD&hX=lMMrdT5cgKswgEYN~Y*&XJ%n-3Ip@@b6f(FV$2L~&*8bxb`UPIxb&7>voOr7 ztPpTYL^M1yb+@o$6+@VerjnSLu#U50`O;fd2ch<6CW{)m~dPcxkw2`JRw zzJ(FC>AqhOJcW5by2zC>2svE5SC82|W^aTMCa0r4Vr$%{%MhF5QK$L=55pU}(>%Z~ z9^;K6Yi8Fzw-68-k}Nhi36eQC%NF`E10)~imR-IG8hnimzwD2W zUw95b*-oOHMTQt?@=M)BUa#A@=zX3>bZly$LDtJ2^U5HdlcuLuGFJRBG}mrdo5u%W z75z8P-%8^fUhW&Q(Hog_W&mJ_l|<(@j!X0PBkDIqVSZdWQLmI*ptE0oK%gQ+!~Q@& z0^i(@oS0QN^MrN@vDKdx84;5Vml07X`swHgd`6^_4#a3Q)ONK>xY&Iz~{GzRw7!%Xt&EIB3yT3 z9#Qb1=pmYS@*d$fJ}nQx=!`f*xZv}~JNT&oEeBb+7y0E+iNWVID<9w~9o|w&uC^E-};3sFBhJ6XjymqH@$D*u4hIRLT6c2{m?7sMa#Fkf*xZaE4$-aKwN`goxL&n6OPAa_Lb9WyX6BJkZe z@^8G~n7%oE!&izdRB)z3#Pawa_nl(pi4oc{qC+%~#8+B^uc=a)x!9wK?+A+0oj;#` zI*pX2*^PeLn`QFE1JD%i9qS!5uW%=;l$od~K!^6~ML(`IdS0Ak#;-`}JVM2VLWgnL z9b8K$2n7TM67y89TPK*QGe(nEnS6}SNoJ#tQAtfdp1MVYOS@j>EN~BfM%_`27mgQ$ zH*#L93AJg=5$Krni^GHA(%{nh^3A2_FZJ~h>?2e}R6SIIAmX4~R6HES_sDVy8zQTt zTtA9B&-N~VJhT}^~mte^zEjS-( zqqCx`iM{Oi=!f(-#0nJ;Dmf@Gm#md=6wa$&X7?*X-pQ#h|_X`ttrW#bmt>)1ri zQrD?FVQ=ybE&u4taPs~E=c2J729WiMyYhv|LzK|v&`Ht1bTdbU5ms<)C)*~!N~TWU zO2jWwiN_j z;imDXYUY!6^4u7&7vFzbgun#+v_<_?s&aEx#Kh&q#%vrb1&dhz@-`Nk|hwQEHGwgdMg$_P9 zBR>g+o9tLHOzR{F8IE*~Op5GzdGE;PB<;et#=)^@FX(so;S%l#-w)zVpC3NVPZQh| zGIXzW>U4R_Rm&9?oEO|}Cv1l`yGyYr4JWPKWZbH*U!6yhM3PtuXaO)4NQb?LnOP7j z-c~qP(1Q8EQeYP_rC_~aiMN(d73dB$=j}B^XwBRb4_GljZ0N37$XFpg&_B56Xr~e; z9(jR`bcJAx;f%^r>ik&<$ZI8ahfDvJb8qPCZ{KWhyR+>PGP* z1o`|^_NS095?{Xd*3-=t1}W7kk>&t$!g8*S7>y>s>qM!B;e6-nSzgUvDPEPpb7z{a zGO*9u|27{42z!cGrs3>THM>&VWuDN>yv)dp85I!~DTFQb17$}+UB@TsB2jf8g zKqj52B9ME~Z3FKlgO-mLm-ZPoFRhINp}eacvz#yW(eqd|aPCF^p4??3VX9UP-xu7p z$!PhvQ3^s#D$ny#=MpTLb*pN%P3Ze^AWTj=^~N)wruB8wo|rQBzyF~khLk7&`O6xT zlwZ7zn)qbFy3&9`X|z7M&d;V%6H{>i<_vhIt@K;zO1Y(W8PKzH=Z>?K^Lp})71m^Z z*_duZjjcU!`069kTcUT|H}yXqjgPy}Q?j}1*50p`4ij8-Y=$)JBJ7@=Kd(}7D@@p{a zr|Jq+0_?qvQF5r-)@~!I`OD*fFf6ph)GAlkR4`_idL)3$%UZezVg?S?8r3;eiq$t% zedmYfe5|78(yxGoUT9~&mzqZ>Kp(+t!jh-O(&wvZ=KFrGWIMt=EQ9)%B@@MH#ch+w zR#(r0Us_kxRg?j8eG3bpmB);ulj^x&Y+w#k4nfQy*MwW>2$CI2eLwMfR-z!p=R#oD z0FUgu+`GPhONz^OsUPPcL2__v=jph~~Ca*=C*Sq^Nl?FvJd|ab$Fqx3xZ9GB31#U+QghdbOr+>*o^rJ^U#M zvn|P8`$8U~J9O=FI42=0k>UOJ{`DE_eaue5$Bd*5evp`-hhV)ov5?-`#D&!W_q|;d z%_uF67%{84CARPg0xb>fQ9UAzB_m923QRPv?C_K_qTD@gBE?aLyC-4>o3kTtc=L#w zf8-08@k}rb#IX=VBnX)sW^VfJ)F7h(XM=(b9?Ww;Yc?yt-2uoFDHmYOPyJj75N&pw za*wA~jH($wltKsvuxh3nQf6{;F!azeGR%_@3mAB4=?V1b;VBgBehr2}g8qI1{SnKA z{m)Z4uS~fAlwlPgUKCLlm6C$~RyKAtHMMnmZ|7XMa@PZGYSKbQ!&yU4me1JEhS|`> z&d8M6-Nyc*3XFg|AGBy=>TF2nZewlh#ODs6`0EWmX!$|RLP7S|E6!E`3Jp0$GEqB6 zQ!*~**UYae1X0Pz$OIfs%=q4kN&HhC`WJxWy|c4D9}A0{n;WwmJF}gmISVTYXP9C<-hVD$ZPLzK)@}G9ZOr4A!E$p2w>}<&%+BGz?b8!YxP&{<>-+zCP z)70JK-=1up{+SkZf-DbDSXh}~v;4PhXjOp+DW9T+yQ#INn1u~=d7ym=a`LhX{Pq6- z_T=9lAJx?Ow*Jb_p8Q!;)ydRR)XoOlrL*9_JM&NF#}EIhD8TYC^G7T3_cH$_ zg|4$8ssPJ>_e>Dgn4uDSE`(vE#6(owpX@Cl31j!nAs$I0h!>+yEA3@}0-l54ajP{hXYyl+&$8iTk~LnuwkZSGX(S8~0(f_Tlq`XS_?#GNdBs z*!cOciG;x^Gc#|BoLkSEH%6^NMSocAOc-fIRj~y=a=PyZQH$u%zyEIRuSW=| zk^Zly8FcKKKdZli_VBmxug?5oX`Uc@(V&2oe-mQU3Bx`;Lsc~X_3oe7{?lS2JpNu1 z)rI`;&1Ax$yq;zdj_8xg`mJL+gBJ#s=3kfiACHDk_ya6eyFmpECk;!`!_fbI=r85p zkuDW*_?r+00W})tBM^2xu@(FO=fT^vGaaSDHqa|Ji!GIr?7Zknlm6uP*>gjkIt+&? zS-)Vz6RqbDxqt}h=Q1H~hR0D6^xj3|~!Vt~mp{qk-|CMwPe zcKgr}dX5lbj-I~Z))))_>HJLCTVhYksaMu^<0RwCU)oxIGmeRT)3_Ei7TfIFK+$eO z-lP?+$Mh23H#1DS)6uk~l4&D+TO-vklP9eL!|<4w((b(u)pf!?sbdCz} zcnE#42c{T1t5$nXt!8%eqHR3l2&e2|N$vE<(NL6up4*0+P1Wm|OQin4c7SFOqBQ9S zE!t0b%P_h9y)q}@l zCGf}n@il4jQbl=J704Hq_-jBVkQ2ExqwV%Xg+yd`o=k#sI)!-X(ge4@=rF%0B_}72 z(|vM;gMLI;U98pniF{6p2%?B=OA2&-ef`q8Y9&%EpHuvU!?EEN;-;o|)Q$9!#Qddg zNhK=fp<@L#+{b;K6VKGkb$(Wumc(bPhH=p;H$OF6HQFzBl^?d7l1lvi@AYe^hHnM7 zlKIt~Z%;_JMWr4gv;u)&Qd3iRAqm0|nd(uj&p8-Zy6jo^wHKXztfJ z+zzal*%of~iv)Y7DU2s@(7mMe>GA8(+i0o=?;LDI4kXxx4+ET!p0Amv^EvPNSLyo( zkQa}BmV)zmUoc87wk=NtTm znk6*pKHDBZMx^yF?rM+QkE;G|TSpzI1}u%J6cpTvhd(AAXV}(HF-+eW1uXRN{9#-0T|YKA3TJ7 zcBczOWQb?(nW-^{++8}|NmTHA?HU|9dkf&Q=mZ7^I=NG;x+L*Sa_ry7Sq^sZZ-|b(Jjtev@s04i@G(zSSY45h9jz#{bhL@R63h71O`9V$x~Ws` zhyuDWpqdYDB$vKrYB5<~A)W~8(2KnIb++^o=@}tyEP3KSUp@DvY2r54us~jjdp|9F zTnYT@5&1y62&I!C<3&kT4_o-Digzk#xI8fgNjmPAddD_Fixnl%W3Op1Ug}-@{py8bR9Bs&p)YY>O`A-y;4 z(`U7FB$A&U;0{Wpr-LVWU!VJ3VfV+qin8Hv*m#dzfZcB}oawS3#L^x9Ca*v4^<|4x zV{SLW0MJViBq;BsG)m1ZvgM=z%G_Pcw|!Igu1UH+02q-nX*@Kms;M`|JK^MUI*kcz zE8Y?3z&ImzxMNfpeEG=gFw>5JRFRzDg4@msUMHHR;g>9K4a$dFr}9<1hd*taKZ2js(1BeTA7fFz+5Ke69yL zeJ$67G_3Yjfr8IvJDir(gP1K<`CVssL@NC>n%eo+e(3DyG|R2L+>vHOWVI)nYw>cP zfQGiU^Gm1eD-FR%hVmAcDW&$!BL9KX2XF$9=glXCzW6i~Q*R3nAfazB(E<{}${-o_Vnwaa)>)kq7t_+ulpSnZ&9)ZZsbm`|Mjy-! zxF2=gBB|OX6p-w^a)G?7L0?xbF7`aD-=Bu{c$?R1qUm|YMuQQx^4c#cKWiwFrDgXl zU8rT9P3X~#c2UO52?FGb$2zdL`f!Lp3x*HPag~IWZQguAsu#OU7Q(_~p*Hp!f5z#2 z8hb~icd!^!d%mOxq5G7>Q%Yl_iHB)hLRWGPy?SvycWH|XX@QeAbNEQsb1lj&4c6nb z0Qu$IDnLn)zV8hIIw9BhY#bRCW4IIwisPA(43oIq{C#5ypP+l6BW4oi;az^Gjnac< z25shB*5~89 z5VoO0P1%L9^8=zoA`g@0>sO0~cBh@VbJZOPJj&f`Gp0D z7!4i^c=m+jM*4!Bm7G<~q_3 zAlAF{vQf|6pQ7(@ihV_G9*n7|$Vfh7c_H*8(Fnw=o?uF~9QMW9}rJ0!`BEyK*?C4E$8uaYrzuWLHzG#!XW&TF_@T*vs{Te{o+or``nMu zPG~Cu+;Y4a)u(|TQtE_XAd0Vv{I~|)5E!{^b3#LjHDdDWf{ zoU5LB>GiXE{J&|(=>ZknKf+xZ>#V%rD8d<00+ zRX=?M1s;m>H0=h%eT0D%zr!@TC1fa2j#M-u_-#;7n<5K^tTCL(C8EDW;&x0Zf+o)D zWqYKpABsFl(0*yHo~wmC9zAxZKP41d^S=3F`sX;=p#92El9fLDV`7rXucw^?DFzQk!&Ln2OK@v46VsXGgBxF*<^Du9!?+N zz+sU7V>G_d*`8^$(*7~o1Wf4QDdb$HdJWwjJMbHEoTfOR0c&0!+}z{|l=v8Ob36N7 zD+vJYiPQBsNPHyPo?_^;tlqeJU%?UzP7ggm->-4~StmqM^Zi)@!M{zZm#p1@1@04j zMZT_r`1%IqS6(RO+9klRj_l^wkyqmAp^J_jC}46Jd^pnVe&LZLJ#f<`7@~)amh+go zU?ER)iiBZ}$EYg8_B8B?Kq4a?>>(Sn04;$@q`H=dk?S)?ttd#$DD5Qj_6(t5o{Zt@ zc-zl|yt}tDa-GQzZP`lq2X6&q0s&!DrdsKj-PJB7t4sYujrKdGU}C{z1$Y$sSy-*KR>DmU#(@_|+fuhls z6JQ?$30!r!q8`Y{9xr{ybz2L=e&d#FULfm7r*wKMuuRcorV8dIoaD2I*@ zoivcA?tbfPpT}#BN-gRZCof7zayRxZSLgHgm0C;+HnaBaIAD>zLHoAi11YOK@lBJW=^JzFi-@)u`}@k`GTNObl98^GG_~CNI@HE zXrW>90@#Nb{mO-PhvtLsW#tY}$-&Q<<#-&Taj|bo*B^mAIB(Ug#$c!e`U^9M@gRr$ zh32Ci@&+~{5Rz`vaEVLL2?#Ji(DW8kno58nr3i6Ujv0oZEDkraRFij8hzi$id_;Aj zT6`L6ivavtsjpI=t~07=$TeBSQxvcS4%S|du#m1?O<3_XoshW>X&bM-4UZf<-=;1+ z&(G5j5_&`^bZWe9yT5Ji&>t1ol}RBnR}=j zp17Ug7J#+wnuj=@mKW#@#7X10Iw@myU@Jw5H#*9!@{&kC zJ|KH2N9n*l4_59!)%b%AdNK4}`rYsDrrvaa-A*2xg_;A_0&B0-JCrT-7*KU=eb%A1%5e?6jp50_=_Bh-!LExh^aPkB95K96g5`S?CV&FU`^ z{|-ID%#+wZu4EjWOdlWtS-|2(|bFNY3eB|B&0FLFG1v@d}T&09qs?gI$T$7!!qm~LNolCDsk0X2fAq{|))q2;2@^u8hGHLsZotMT^2svP#RkQ&|l z8WzD2DItZv+Vc6gzfEr%w~}Exru|@bSV-J`%~Rm+G$yg-z2IFUJC+_8Tt9rIAGsL< zLe|$8;;2}Jw@I!SgX~Nke`i;i@eyF((8-MTlXBh$b8aG3!T+|6?b`@@sAs_f$N00g zR$-B4v&#o0Zkd`I#;E`=TuRW*`~e!HI*6FdcG}-*^9T8ziU5hH0omQnzFXL;zR#|U z^>njTo1WJ$dYF%RD0TFxQ9pVnHf>+M;p5bZv#P1c^lt3d-Eu^w%oAH zR#P>WUD8k8-;*^v?-ob#I&C~PGB%13HCcS=w(!|-xGgY2AHO&_4E$N-BPrY2c~b*R zE=i}pC&3{Q^6X&u9%j~U%V00TzeICjgC`?4u!occ*3ZvR;Bs%Q`|g^gl{88~5I#qU zH0U09%;mW-L*#S4V7!+Q^z0@7(v-#6@vpE>`@`GYks>Seuv^VC9Ueatq;t~mx<1$V znx3ZzMGd;G_9AvM22!w%N|X{SGO8gTFfgi-4`5+om-g39i4SwidxY8-H5y!eB)VmB z&-NEW&cWVD%Ak}$nFQu-Q%%zAb^NgT$@0a?OQ%_1-DWq@46m#A*OyM4Sus~&HU;Q- z!bU1R@tyBsZbd{^^X15F`u_uy|Gs`E%m(VY8)d;(-8~ugbjzivleygajphzG25Ak` z2{ZlkZ5x@6_OC(L$4FE~{(ChTLP{Sdo>E$Q;k>`SoFhfPcPj&P?M5ZNpQ-}*CSBF& z-Mc{uOEsEx&guGtp}wifCv#Be8pQYVYkYF3UAMyc?xNqxOF_cp&s7z2*Y(kSy}VR7 z3|iQ7+8o?x1K-NiS}M0P#69N%JkUl|5y)P+PsfKL+D2S}@m zO`xQS%UpquW+@ zvRQk}SJLizpzl;=c`2=1_biS7TSZ`o(vT%JlI&;r2f;f>yzkCx{Y?7|ru@EqsEnCER*;)m1 zx_&m8#yK~wFCxc`Rppyg#VnHcs*>GCzGYt9|+(kQs=Gtq$EvXq8I*OT8Na4(sGdL_5*DZXslaZU4-HDF`t zmWWBaZoFgQSKh!Wz#=~@l%W5Ivud#I-O>IzPericnb(qFVgMiso`#hbvtVWA)ZrKN z`B~B^p0R`oY(O6yHY+ay;BX7QDT<-Qnf?G<@R|;oz*Hg-InUfliN~bziT@fL5!0h! zaQW4=(fv5vA#~7;Wym_k!9OCX6$A>O=yUg#Y|!sMVM@KnH~{8gG#6&Ia#~G{W5ENt zta7Ba4DZ;$qV*rvCd=ZG`}_SIibzaVJ+-|>xky+ZO1&)eeVcP zwjLVXvzkiW} zJ!Peo-0hKh`@wB*o@mtmgN%JoBR->iPTH@wN;!Sn$9|n# zF#1y}K+_{~(MmMLc&KqHTAF=SG&q~x9YT!ld;dSDg6u8d7}Ia2&FXD7wXNX0A7)$^<`uO zlXt1`o{leHy?S-E-fP7cM~#urQXIfLwn`~GH?+8ZGx@dbb~4f>*oW6f$m&D7P6idyikhp!;5LT-EHkk7aL6^m6L&>K+QqmC1&ok<=3- zr|WsI?6bAASC2j8e>K+v4~>@MVX;3N`vYOHXMz%g@(I0Crr#0wlNKmap6UAjXHPmZ zP@Zd5hiCPe{X)@ZJ2SK$S60^F)tK;;DQL8NQ)|C#_76f*`OuE8?BMSPUa`Fn+HI4X zS5^IQ1`SH(9-`*|GyMNq{f9&Qf6U>rcM@}flZoqw174YHjfZ=T+0!gL=Uq?k{q=A!IE`v&%%Rih}1IPYtrQ4N`0O%fY8&z5_H%rdRglh#tGUojl=4BeE62oGsS5p&R@vR^;aDw>_n$+q1&j5Eb{k&XOdcNC9=xBa2tvhq zQzBt=q?jT3dh4JgdKC>%zQR}OJiB?&|Lcct^1PcH2-i%{gNj+oTkfp%Ua-9NTwYIj z8tscQk2H4WMS4#PzHB=`1<_{s5c(XpO!b;uEngb47b4 z8VpYZSzou^gwW2Q)FK<}a;vxu;h*GL z;7pb4b`~}A5DU5^FGgm38apodnd_nauTQj`ekmFn4hpN#&LMeIJkix{vpZo0x1&}O z!Z)!?7@~$!97@>1S;gp$@{D95svjpH!lzoXL>jqRrL_3GB2we?tmcDB9Ul=96yHduWtUF#F-XwEC|5*4irnk`86$bx4DTf#hC4R! zi<<9@E;5}bTt=OF+Xg9eU$Rbm!r|hA90*c~TgVn+fD~P50rgZ7ytH%bcT9buK{r&8 z|LLB<^(flan}vmi2k^R@Jy&lRGUNB-TWXTuw#|$2I;A3GuL?rqr9JDWS z!wEFnfjD~g?yQeU5p1PQwYF2ZCmTOH_5B9NXV%;jJcbo7lIt|;yb*q+0SeCV+3f^9 zPj^1!$RrSTN$iJy8x;?OT&?)wW+_iGUzmeojs0v{5;OdM1_a@5+RcH=n(H+++PHiY zH>}s`eA#`|ZQbh5B=$R?=x_Vx7r4?vv?VHy-HK}sEIbDIr0eC}IZm37-=l4%PZ<*t z#AZ6M1yg&j>t=A0|8{S_sK$J><8VdK5kzEOLngQ zD;E%4w=Xl)a3E+e4eQ_g!y|k(xiIZ}M;(DyuF~`(o>9l7^l+{PgGsAleD`NhWLEf2 z-z)vBfPGso%xfwQxN*a}6G6fRqi{)EgS?8d(C6U)Tahg7Whx1MzG8jUc12iZF-e5J*I*5s?r^I z`}>GptP|*&G>wDg?+@UzbV*VQiU6++d2$YNO4NXS@i%z*GGC2_Eg(6PW?Do&AJ(vVlQuXfbF7zMA^Bu zsH;FHl=q{hP`$vDc^DTnLx6?#!oYXfp?kHa=5D(XGuq!`C->D`c~Ia1(6ha#hn8P_ z%}TFIT2@x+S;L_R`sTYaDY}@R-Adryv(Gg0NAglGYcRoz*$4oKHBFIPr#jo>)T$ z?B5Qv8vTM=W@V*|sO>)Dr5f%=vECfU1Ix~lR0S}?v1#uU;SrU1QrM;H?@-JQj%~=L z{G_8A^u|i~$0g6qqR3NG4$WyoIl2zvdL?==HIPX4JeC}q6$m>H^b27>*x-XY7gA8IKhKBrU(W}H z?tlfh4O>=vo29fE{UWsRURer#R-Sqm7?0#+FW*%ru|2c2o?vvVJ7AA6;9epoJqEYm42h;47g6DfKQ=2!(yn_|O zp6dLQq|lftoDSKunI*{bT0xwLM4Ryu`yhX584c0tLrk*IGis7{Ujz4?0^NIzbI8vx zliqzli+I0~%d|6PTS#lcTMo;wP?P?qkJe8`yoW^ju;}U-Bj5NTyK^24%a#V3->fsZ zq`37my8N1V;k(j+*#D(7L=oyQdAZdm8-b%L?7HQD3-EgIe$Ezi+uc=*UtPN1mur1o z%pZE)Pb%ce2zEcW0X*r|+DM);1ma|GyZqn$$Pgx|% zI$H9oRJHsUv53d_K#%GP&?t!&e`(jChf{R^opvV87>a%hc)*grz$)s%-F6Gj(kNqn z8p!Um@_J10KXhA1HB<5J`_)>fpFJm~ccUjX_tW|L`DUK-9-#C(ul>-XtaLU_88fMa zpZN1`pFATc-w}uLCTrq8TwP*DMOg?Jx7S0>zjPO9s?!FK3=FRi9-Qnr`tXQw88n?R+r z%wt%Nl>ZoEDFlHDCI**@_lW|#Gwi&*1=W*Z>x2~|2k1v)d_;=P74OKR!2p^EHxvQF zyLXOmqcpD;XL(J&VyVkVA_v-1k6_iP*=ThF3;8EQ3WV;+*(I&czAjGsO6usFK~}i_ z_FoD6qO5sTCZynbvR+c$X#$1)95Yfj4QKwsum)}IP}KfGpjD7|d%Q^5{jSfPM@)$J zlR*4mVJTbw53o3UP%Gv$$4SPL2$lKQpjpr2e_5qBb9<9OoDq%iz-pj=KU9C=^r3D| zm{NC?KlrbX1e7oTzs(L|lj{oolXvSw+qqR6{38I?Zu-ExWtFS4|LAuQ56ZjSaGd^V z#-0F5_nF63O6mS^8(c#JX)|_Rsed%{f9LSy^nO~ZT4p-6Ff*%JsU`fi#s5iB=(T#% z&?;!Cw&}#M5-f@MR3xXeTbO)lFBUHBKzD>4LB3Ooi?* z!rnih6()5fzoaKEF$iIm1+(9}6z09%{1^R|&wQ+KIVdHNleyJhO`KJ&c%c1h*UvvbcFS>x)1OHiXxr6%O% zJzHlTMW|}EWc`h|)?xb0ls$=rE^Yt|8{MB9 zq4E5G^a%8j(PG*srvn-&#O)^-kG_zR8x2T&l)A%DATugQCEK`TN7~maa>1cW{2JAO zAs`?iTf*1)>+Fnk5;$>3gPyXo=u*OJdS!ycQU69G?NqNdrmBb4c!PsczYaS=^@w)h zS906?bO&!&ykZ3P&b~yfXkmqSVKilmng|s+w2|M`vDyOR8_H7Zs#>O*__=oZq;79- zq`}Uqb-OLEoM?zjCx%H#F>szw6sn;mFw+`;!!TaXYi)7avrR4XSdI5g+>PBU*WZ6`oT%{L#$l~0?^iUudUqf;><*K7{p^U5~!=$?2?=~M&yjfYX z!*rvZFhy5N9$%_u6g%`=0EZ{Ve*oSqZ-VqryP*KdGQj-}BspljFpX(AvGJ5V$WifT z+a2d=pxLgwcGAMcYsGEdQeAdg9@H z$8RYJy-ZX}0h6#XA7bJYgJz0SAoJ{R-|lCS6?6j{Av8 zneW%(*}F1+?$s=yMudn#nX(p~$ zWi4WwF%EkOlvOa+_&9T`g2gAeO47e8^myl+6T}%NcIvy!q3_GK-H%Wh3zqRT`?B3Q zZY*vC4IHJS&PcO^btgkkxB3*vDSMs~sV65V7kjCelz81485!G4BYaS$R~M~Lo*3tN zI@z1r72xfs^X?KW=EbSi;Rk?5aqzlX9NQ#|F(~|c9V~xYnWdRy=Gi?BJqWA)aSQ|) zt58(`@9j^Ocu?%jMd=wYK)#5wq7Y<6kJygbdyaVy8gu}a}2g3EtHa!CcygPj(yVZz!`YbTP50z3g9~z>#US04zbhK zQ|a5_b*%Aaqhha>$ET4`_Fu4s@}S}lRVDd_O>^nd5}Ho7hW*)RQAS;g74iT;>sYNm z_;%29r&RzLL7r8u01|pu0^^vJj8EGzdesYY-@5DI$@8R8FV&0JE--9*3?J-2`H!9M zz2x)6M^`ygu6O3WD9ThIA!cUhO#Uk1R?*}M((!L@U>IrRNG>6X7nK}?qwS*vTK)rSqssKq z&lC<0EPMCHpkL9XR4rnnDP6nn5@>(yM>3Qgca_BM`z=X2+6q>FtfoO)E>|hf5vTQf z_L{B~HUKUJqtMXb4<8R?ur1K*d`{YW{ADor1N3{4qCR0<{A;(vna>NB?_wMDJ=6~q zPy|fgMepUFSgQ42hTV0eDS!$+zueesAjFVHscp6d)fx@43EV6h3~KU70`!)PJ!RXy z11b;Zp`ZbDq(3YH#;e{UL(EUb_WXR0iFrdB=lKS7x?lXCpEk; z_q1Js+ZqLh?R?>nTJXvt1J>kY7kHkn2?9{g)|8Mw(38cQp46(#*;8Uvu;d){%vE_I zaoNq8?CDiHgVOs>V(xtqJ>a`Ph+bFmdB;604col)Iaqh+=kub=U#X&!I&hB@yc>P~ z8x5SdDA(`=hBo5#{qm)oVHqxBKZLMXw3gj&c$^q(!X~M3TIJP%598(_C#R<>=PT5k zpY+lj+}15O&Ig)W@Fr?3^^~7v1JyD%+7t3Qf^Q2WSA{a zsX-q3!AO`m?O$>BGL)FuWDpx$`KhgMOt^ZOrLY&wm+Q;J0jmC@A){f*+C}nZ&6dcv zm=O1n!g)S~fxO32zE1emjdVbWE+$&GFf$4j6^+r#S_S0w4;?;)us>fcHy<`s?+&0_ zGWdYS`tup{m#)V#J+kwM6t{FH8E7ixFX8s#lWt1wUrWvcK_3xb#9)=h_ny`Dum0kn zx%;sYXid6lOR8O0GvQkwxp@~r-fozRkn_?5=ke`>PMi_iqNct5Z((ZeOyQ2__Q?i^ z>*gWGH=9@!a8+Xk4$yRP%3#4;>2&%5r<>rX?u=tjpo=v{=fiRKF`vt-D)R*JRg%-C zgKc`uV_l1F@B5?x zV{eLY+~q^H0%P5?oI6WDKZq`EY#>&wq~jmoy#?-`{{4B$D|@Kvm&b`paIv)igCnhO>vtEolfKHt1}Q%#4fi) za`Aoe5D}|i-(q*?>bl4Zuo?V;i)nVLeHrf4XMjcQz;BlIFM66u5`mc5R!u3#3-oiK3R5Y3y+j4B_;ebd2_5#Dp z;=IPv0B+6rsT=VVXWo34Eb^=iw*^btBtZhEfa9 zGGn)~tV%V%zVTKE84hHd%>ML%?V9`?)sx9W?G`B1yB!IBKiSwM=(aW~SEx=qINkIA zIDQstY6LIUC=HE;l#q9mWxDE+ks#yYyM-&lN%2zAmvuJlvpE0Y6*`r|=kp8mE-Xom zNQm$hG(TK1G)6C}tQV>f@L2T3rXm+k9VO^AqXZqEYP(ubws6@&;nqxk8Mi0-*yy_| z`xzn6`%#=Q)+LY#6b5RPwCzRBHlXpl+I0qn;hErk(WV#?gZzSm4%Lc~33SoGo&7n2 zI?;-d;PEj)glCZcBa``1GSB1NUn|LkbCNR>_2G*;0 z*1Wj7*jc||FHp=Dle#F$N;Sv&vExmBEB#G!U#K8hm5;vb`vU$~k1OX^R4!4b&`2nx znT#S4$_}VO?{2QUlF46_=_{tKM50GR*fpq=6}I?ZOg0(2cHx6`maL}woam!v$3(cx zgR^=Y);4HiNe`NUccF|QH0~#R$qra08Va8i}rSXfx#uCGmmJ|#`NJkRLfE}hV41I+p%!>R~3 z$f8pN9U-AGsUy7q`M&@_ZVJJ{4|x%1Xm80B$II}`^;!aUi>9atEAI_9263KTE~w|) zV=gc&R_V#Z3f9||b10SL_MZ)_f{F2dob|C9f^e65&jzN@mZ46c|Ha;WMm5#+eWOZI z5djqiK`APTbOizFDAJoq6OaU@_YP76qN374>Ai*Cdkcuv0HL?gr1wx0LP$6p%l%x} zbDp)%`+hp>ob`Um(h!F1nb|YHHviqU3S_aipKVR$Y2E!X_nmt9IlQHv(g-+*OoLS6 z9LUpFqp+E*v=SBja$N&cK0MeWs)F0YP3hq?<@hzH{Dm9_NH(-vgjuyH3com-Hcz_? z)DOZ3;nkNkin2=l$N~KYh{8+pvYjQRW8h>yTV+dieMKSEN_9du#0)1=>Frqes1jmJ zzMSt>sn&bEi!+z21p9!=5y_rJsz82emCxk9*(QC3O5PEEyWiiuKZEty3qMqLIJq-4 z3+=Cv7P^aI7g#8ft%$Q+F$!WNyi{Valh_#OFgJM;!kv3BfQ&LX4Hd9=f9W-=8`@Pj z54JBMoBZkHzOv#h?Q;$(%tmbT>*cLY^J^pW=e;r&qn>iR6aYaC-%Q_2Q(qay?=4e@ zxrU=V72e>Mp7BCLO>oOau!6EOZ)-APjE`J-vJ?2WRW$Wk==ykM;CL!g{CAF(hic?e zy#YWNzw<;wx~szKvAwyZc0dtd>5rTw@ftNXqRP2kIj-lV%B2a3&Y+$(VMzbX+RjDZ z?}&D)fKbNo?dg(Ts#kQEDzr}A1DECq5>&t$eICn*sJ*cUp^L1?iZ>QzY?1H7^jx;F z=hK}-7a0Y@RV-YC&{O6wc(4${M_(9olS7?D|>ps?g_Ss<45xjV~whVUc!YJkdp21 zYxK?_*>v2U8n*Q|M&Y+Pk`fEI?weYM*sU^v(v+JbmwUQ zTLAyswX@OjDVZrvdm_NvG~R=U^lM%CIwW_=&v*q{gD*Z zT(~J}Z-|m8U+vRwKvf}|r=}QAgm#UN>%7HBw#IzzdMsJpacTK)c6IK|WZA`t{iK}_5 zsy$v-fsai^=zi5N^s2FDKy;6ge!0d@gq4z;?$OXj_Uy$(G2r*a4?E>{7Jn2ex9*Ai zOQo9z_W`-nOZsIXiUKfIUe!mlL_g2_&zner-IMn>f(Y%fxq|(^wYlhD+;@a^v`zX7 zB%N&iU_Qy8@t$a-rn|%r9Drl< zQ>{@A8*R`95ts&jT7DGWyMfct&f%1We% zM&KdPrH#FmaHG|a-?q^^Z6O87hYNnqZRl>AgrCczWggedZM3X3TgY3$15A~j6*kEF z>qe3%7m-!goaWJ8>#4rdq`-0MzzNuSm79W{d}HfK9H`uSM3Fc@^g?^SJO6>>ZxKpz zi6OwmDn0RUgC-K&(j%9#*G%5cn)Rj5d)iGstUYv3|eS{emDLpFjOD} zkxPhp;z9L)$3oSGM1~5x4-P;ED5-cF8~fSqvp;?$4I!}ryzCG?8#>2$DVq3yAd zyn;dr?#IDRfu;xuP2P0#WAARAyRzw~@+$L(5*Un3%e~;sP@>>ERNeXTn|Cy4w*}Hd z8Tog`L^p)@w>+ak;0HK6WC^_BhKoV98x0->#~k59Jw|8Q7}YliF7b-*)E{9uv@q3-laR~1 z-UC1)<9!i<7S(_bv4!R3zH&EEsxbv^sKC`4_Ahx%c?V2OoD|g@+D=x<_NtaFho)HJ>D9g-JI2(OqpW-4L?$wx26M?`9c{`&2DiZDR5gIz|xD zLD`QTw;#}F&+NE7T{lcR!sGFWb`F4#XgwNv{`ssniS6;wWLH0>gei(}e<&ZMe_oI- z&~`M}pOS>_gpW)kOXUT;hgn*jmDS*J=$Gs7_8qZ~*M}Ts8g6N(UcDN5^`NwZm(3{D zsIA-LWuqlKPu{Fb)S>{SUo5cYsr1twRPAsKpEsZ0Bb!Xm+bVQoVqel_EA*uh36SDn$E|mgU3I8Dw@`n^LOp=;pj_q-a&qjwj7vAi%Lw$6W5WS#HzDG2V;bB?J zzA&T5#%m+U%=xV+(MeO9XSOAWZyr+CqSrz^aTtCyroK#QA}2NA1ha%YoNGbYFN?dS z%OLlu+7_tf#?k9)gZ4ncxq)k4(hHyHP+zbc97O8;dTfw42UO=->P6wXMjEff19{)o zCpnn+JoV|(4YH*yA0fI>aZJXR@YiW+U7-7~F{~(LMXY(|>Z7(^ia~Bq`(Rz(3*?6q zLN67&t7@FX8kHfuy>~Waigu>P?CcT#c>B=?Z{@mSn4B< zKFDQGg!C5stfsis%hEqp!(3N1k?ci&fTx z7x#vt&G949lSbVKW=XZM{OhK@hZ@V{Hww2%UsaqVTfVw~^kO)0tj5#qxbBTWZ<2(+ z7F_!_`;OR)j~Oc_>9Z#^*58sJLZ0Yjw(p-)E6Rr?d%S%+R=;pI^wJAyFT3O3lZ7d& zol(28eTR5K>+fBeoZFLd#QMQ7T$|p;g5%Ja{!YDd^P7fRpONgG$LjX9%~0<%@A=Bd`I8`(ZM5Kwg# zJ@M*I6s83r)!T)fcIK8Gex=>pBh~`lgyw+rbMR0(`n!dE@c3~-{l*a=SX@^bdXLo| zaOU%T&gTz}>u`ZSx+aQdH~Hj%mT7?#iM|@7p1bWs_lB*8^Bwp%rpw@S6aS%4LioY` z(oFdGQ8s+^>*H9B`|4>d$HUKi%gD@J8Y6sIhr?LG2^uXN4By_&*-Y5=T;%UqBkb| zW!6F^HF7U&s#CRiVGDkGXxI-M4&T)5D!;irj5j4_^1k4*HmnYl{KA~=csYcjQ9UXA zVIZmgqM5@>zwzTatCpbGH6MnwNVN*z&$p+A=YIU57(w>fIumBr+Z;Nh@~|{f+^Owl z)Q2Jo+*KHFOiq>f7*4h*pMCJBw9v<`#OKo1>p9avNB}st;c-8n{iGQm(3fGtKbY%- z=)^?JJbBew>05x#xvY6MUdWcOOFaMM@vxB92@<=k@$msXz_4ssd_X$M{St=4OG+(? z<)vdhTui_it3Mp?jU8ZR*r59~{<3({3W3tGk_5l_b~k|XrhtggMm3?hcdUu@;zH@^ zkU18Jrd7V~!&ALJY4)ax23`1I&yG!_kBTL9R@AfQzNU#VjtScTA1I>ZiJ}h+orK69 zYF}*%NVTu=;+QriNzQVl=x2S50M&TN%+#?-OP}6j$JfIo@B0vb%8_K_WIZPPHPn{E zkQn`h5Xs=3yY17}_SgLqbYLje z`xf93*C(5B{e2b%$CLC)h?wod4ZJ66*bUSkVAlXy*Rg34s)K>;juK-%%C?oZ*p#&VH-m><+Ph(2wE(UCDc270utMcTP z%Qwu2iu`NaS@4W?m>`=Gn%>+d+fj7l;pQ^fuInC5()h+$9)hB!Z9CWllAUYi`cB+x zamM*O(zs=A+k{5P1Kk}wJMktUk+Y}~M?aGH+G7CJGUIdIj>5BLuHyxP&{iuJHUGk~ zw8XX~B?J+ZfhU4b9uh9zXGoE_^f>XMe$=}4375sj#Ow8Cq&AKvN$I`vHDg7BrR(#( zA85Zgx~f}8Uap}@0xlc0=?AEx(HU>z;5dQK{vTX();*(FVBj8P1pHbD)mdmjEXQn=@<7i7^N-AE6R9C^_mPgVDb zPfs_@C@@!-*WW*P1@}|zi~GyY#0CSPhZ{os+0tAP8OLwP0G-|w-Hj-A!ni)VXq7?S z_<~g_HL6w{NQW?^NZaysXOznpCOpxJ9M2=q6ghq~#7|ZdEFGh*MmLgsoR%v$J+fMX zNY~Wfe&-5e|1R?aAPz`p1>5yir&6+X<&3kKX!iywe7w?`583U0T!U#HTyG1I8|gy4 z?AvbTX-ZXOOndnQ-R8FvY4Z~KleN4Tzn%HKDcN+|o zY%%C7ft@qTPlpPpENSNTZb)KpD&w8vDEBjjyeDbFtFvl8AQk?McwB6hilk7M+aMxT z0PG2Y#q~D3U3{GGJ9w}`Hb8lO>{{>k>f*c1z1#dYKD#hPSXvgfw$7~o8! zzZf5_(HDh5O%*XZp7>vBiYKgMZ3s*pVX81aZ2EQ+2(C}gC(_aIY?m+7Xsv_}<65VG ze%P)abMIcRr}+HI&5yXLvCnVk@l`v%wQjsv7-Ri~1M`pJ)QwLo;)YAWiSS3dp*dpi zCv)jn-YQIi54(^>t3A6NzT8*B57oUPBszItW`={_zc7hCv$TA257$p;<5iQ0gnTQF zVAho|H+Y0<^Da$2Xc;P_5ZAQB2798)-jx5seNo(cUw|lf-tv7O%?3@m+cV_fhP>0W zoJFfbJEa;C=VwIa5qh}pzUBS%I(!L)q8T$f$-1o9qgh7XGmHL(i&ye`gjVGc@->xS zn^?T1`U9QQ>N_TQEG>^0LkR_Ijp6z#eEk9S`S>7x&bpX5ffq?F zvBS_&KL2E8I!IopHnvKyaJBlcyyIBsZ8CA3Yl-g4&vJ>bg?s-D7|LMVo%}v25B3(K z3AcO~k_@@qeYROC85wF%|tyHTGdfbI5 z5Xj5FRA5uJ7(X~uG}62I$-8P7H+*H;gszu!HfM=GTc3?Y6)3C~%+0!(TQQ{yLulh| z@K-vd5UcSq*SS)NJcpkv#M1GwDKPRzCI*z4iA7ha6|&l?5{U~>YhxpzDXe5cXc=8p z1srbGdstt!ks!)8qwko6EWww}TX7Z@B-`n48gagTjmxO=-d@t#D$sb#xv(SX#Jy$Cd5br-tWI%P21Bxg(mEVW|Q~3f+t67rMa!M&zUjBFe zKZNF=hEVy!>R`2-`P3EZWS`dlQLBo2N0YAC9lq^+J$rMJ()|q@2xZJ-ds~u$$2_(j z+#Ra1_wUr0FZqSXD#dKtIip2)^HuR)MtkVpCojzjiIOus32gta^l>r4SEHwmt^z_8 zCusc+P=8{dVm7^b^#rXlF$Mp#29k4rHtTg;zQxLT4eeC~+a0g@31v9HP5vJlL^5MA zubNG^Uh)M>#QPp2IOyi1-Jl0I9d=6E zc60$Y(n0Kv`)vq~P^nD+jRk3);Zk7Cn$BBiFds~#pu8pF8#5N^AqoR@e<6~Au#ZpI zB;Cy%dCtm}lOsoKCLW1ayvCWyaiaE0y?3W4!_AzZ;e<;ixZpJ;r3xCg6s?))CIhja*4O`whSB_6tAl9eo|{$A{QJ)3Zm`ElSc&Kf*ag>CtkboRQrUbm z4US)TlCzz9@8R-2$omw=43G{CyifeeM(c&vp00R{Zfxr`;nwL(;x`TBK*YpLYzN%*>hgTla<0V0;jt_se} zZX7h3c;e?kJgvH}W*x+*OL^(SGv$O$#^Q`Ug|S*Aavo6qr{7%i`EM?{ijbbD7k_cy zEWUDj<-=FWXEZ!GIi_0Y);E``?ZVW9PN_z=cU`d&M(|bjTi}Ft4J(m~ChrW|K>xtw z<g-_wD4zaHcy0JUT@N zo$A4WXEO(n_8%u;zuRII#wA=egiU1SDw8!p%$$pt&q6PvS)`B|9tCj%wjZZFU3Fg> zra)5YO;zT*NIH zstGihd-09YbMET6l3w8EmTG;Q!_lR2Qb+4B+Wg^Iy5lR1h;RRe#Dz#HwJJ5r+!l1t zS&#$+;Nq~o6;*?yOQ^dP%7jbt3Hw)dxz7V5SIjnA_D}oC{eQ!L!aAWs{+Le!fVF4r zQGRlnB-__EmR`|gaqYS{D(Mo132<(ql#Ti96IK5&8;RjHdoO%98qU4)=Jr=02g4}gdIP8|TmjT(3I|nqSenmQ8X$kF z=f~7S3z8V8Ehds@Q2&qk+~hy_{M14GmA-2_sd8i5cDaU0Yd=|Qjbc@*ob>!w$E_2@ z-c~K8OmTVkDbf4)EE!>rHMCo-yti+EI3v_~mnAG(&Ue!7w$z#P^F%Qro_5v92%Aoo zsCet^>phTeVTp*$De-}D7wu&sY{x{IsHo^(V!A!S%+&0P6!SCYTpDw-Atk6I<%ZCM z?E%7cqf=fGllT)pq;Z_t4r}Kq%q#SjPv?+flDBD>5ljN#Gt}PTdNhB=$!Db;ZYUc- zAucqY?=fDc&+94q%2F{`dTX0fco;0lMD9xaDMZ?q}A=YQo!3c}AP z*E4m$u$D#dONdj94Rn8c=}G-y)%Edew(js=Pfc0({Po7Wv2?Me-phhmEVU##^pnS4 z?eIym(@faSq8f#)S7NPP1CE*_kjf{ed}Z$zfzl3|yXP)NfKL`4ioU7o=Q578+FHMR z`yb!y43P`jk41{kA|Cep`Rgw=x0DnWD}``6alVMq&H}>f>gp1xh5A5HCAkWU*$=<` ziRNsBT&1vvsfVc(D9K9bMausBy?KX~8CfDZ?CL(m+3hl(dI~?UEm}oz5>1{b(8#00 zIQ`ErDrkmm2={Aieida1IWn?6aJ#r)?m*(9KGUjs`7Ft0sWZP`{IV~9>heUVJ^iTo z&)z#jBtgSGw&$>{?((0Ho_;<>;_Qsde16gG4Y@OZW7lSr-bYQL_U+(X;Hr`S6Wgaf zo(TjC5Tr1~m-ehIy8eFn`TzP;z`wR*&on0?M1t1t{HsSmBp3eAXMrC1KmG9g_Wl2N zKU{4vzC#6E=;aTUfIzQgb|)BY^JFf1m?`tTPFq1x5plCcU7yID}d3y4?$U zIrcZEm3*90c*oqdR~O0MI<4#)?jc>RbNG2hul&0IKe3*D*YI-9X5z~kqKBM)x2qz* zCX28-?CcDrddsm-gRhmpb(-di#ck<3wrB};t`0&Mzd;BF9Go2f?djg>h8Q0ASu>21 zrD9yr4Zf$1_zF~)bg0;Uxlo4B=v)oS-%CfFrmta z2KViHlTE7Ff@1HF>yD!jR7R(*{S#p@LK?6`JQ8-?|HP#Sq?)KBA|p+X7C4QM$H==s z4QR7-OndeU9~el6?c}bGc8neGpcm&%$gP6jWM*b{4(q()YzYXtmP4akV_H)$vTg#b z_7@Vij_SZDpHdeb&1a!z5V(wienU%B^AkZ+Tf1v@ZSaM*_L3fm*L@~P8VAFM(GBMK%%8xVnXacU5wJrYI)OXjd%>{#FWX|&=QH%9w{{o!Vc*zJj`dey!vu))#+;^O;Ai(xY)_bHl-pS{FG!D&h$-7juF$Z7If1~L5#cOBn=0eK{ zrZK4^*Q}rsn+zLMeRg(nP2A<8KXXSISmECk!Yv*Uaccm>YSy*St~1ug+Q!xq7i{&8 zf?*62#szBZBdqkQGKeZAjbm0LLVH%!!^`2l;nmfT6_l0B2XDF5d8>-DPw3p@YOyeq zpT4Zy=$Mm#VqMT9__oPVK}m@!Y0lZZ=c|xm7}uk8_xMWd@?|dF%9rQjyy^SQ*KCOz zRP-exzl(oB9~eR24u$lM(=wj)_t)2;dLM2EMO!Q-waK&c`q*xc)+Q7dMqyDs6~lVl zKaq~Z7 zQwOe6Z?k?rM_Ir>5tH&Ax$<1qRdl@G#;N!?&0EIRFRF2B#A6#aDj;bDQ+wh7G+6&>tohN{w?Ya zl_;I=eQk=3gK6ojzt`Oo9;ss5b<~-OP$}{&k6$`Iuk>joIt6J~B%b%a3q!w&ct92W z-%+!LAU)f@1O`4s!PdtMs=mg0R}g=3y0}x@^7E+jf>V9|#`^}j=iv2*cc~ikI)w(e zMdE~<`yQ;CXT?psAux+}ys*ocY^QSjtK1-;)#Sr)usxJuo-KwScjwB6`z|mu3(l?J z-HH%}HCMCIt*ac=O|dhp7G}iQJ`H$}WdaIP%Ir*w{0;Sfl%Ht^DZ5Dq%P|X*JbK$@ z9Z6?xZ5q$TE`TG-c?&7I#HYbKYWtECsDZ=xcGi%c4_BXMbnk&ORcvMK6+RBANwonu z6vZ`m8|D4VEhP<8zIsiyCtZOyl+IK;@lufSL?Y$MinxbJX^r)8EUD6Nz%mktcBZ#t ziHJ|u$2APSo?i-f?T?Fus&-N6%&>%t?Gzitu-b1`Ch9K+P)*Uk?X>#?p&k%%^?gSI zxJ$3TDI^hT5~M4|+CCXS+N&IU%Vul+V&b`)nf#-%_QK~iqxmm%x$sAnVcv|WQH}b8 zo1zX^(HIA6%>%NyE9$?`Q-lya@aoWvbZ@yz&^HZ>9_?RUo zC+R31bBj^L`N7*s8)?+2eyLYUzAmhbuu@qm6c!N?v2jhI!Au<*x#p};5HPy1v^?Td zG}`awG@Z!(Ijb>Mq88@V&ckQa^=)Z~Jm)Cl{U2;C>l-dUeeMEt-yG=@&s(K^iR$x^ zZ6oA}z`FK%TH<~NoU-reND%sZ{25*?l$lk$ku17bBA}PTymiflFJ(pwJMIq)!)N6q zy5DeY_INw`Ex6*{O@CganDy~GX+iItKl=1OlqOL(K?WXUmgMI)THppxm-L@AY6~AE= z+`n>#7e0>DF zy5$~g8U&>K02AXT^31`YVc73plgcO0aO?Qi?7w?sJn%q>Xj6_kiX=C8jWX?y$?Ub2 zRe{Mu6ixPu)5>webwhfyZdY-N-`(}+-6$@p;?W4t=*oX`FRAW}mp59iM+fcy z+>yUPka;JtymQyMS={^&c?5d+M?SEu{J-5SOPe*^;xW;M^2dI1j7JqVgQwtcZ=%v} z(!I!fx>jM@2Qn=@nkc+!8S671G)p)+ZY6fiUUDF=@40~i6{KTh_LX{caesYEtj17f{WR8RA91gOQBUow7mNFe}+U@JirViQQ?4h4TlCwMAe`gSr<(+ zplPMw37T43q(xeu((7VQN5g!M9r8H<1OOiB*OlOArnDlg^NK)9C+YDnL9jiV3(T@N z6VJK0xL6TbS|mr=6KxR*g#(rS^z4!**s`FRaB(*;rCPW3+kw$MU&r@OEY_JiFduSs zI%AF<(3y-7CjwX9W~O&sf2R<)fJ$NGoF1KobB26HN0$J6aafmEU&!-HG=v@rQzd{V zpm@#Sh>^BC{PR4U!VCy++!|-XBe4A7clWDsHi>(xtShZvV75;pu`aGM%8Qk#(sCAk zl$n(gM>XS}xnUeDhTqT_sMY%T%5`V4nTwt)h+a@J$#X~7ozM#F%D#f-vpAM%o(Bnj zT%ZWQo)^_M1M=!UCnJ51V$U(Ihu?JJC7tQ64SM@_`mZFo{S?s(Vn3-0s z{@HW_Mh%OTB*@89GO!xSC6X!o$4bofrR)Xj%rGnDo(t4wqKKGD5LGfo#GAZ}B{%sV^SOE6_NHy4k!*0JtwAk35^=1Zbi8Mm=@vRI zSnR?nWM5x`UteOX4vC(jD?rL4yDMM3pdZhv1r@3={=QKy$sd&J=)WJmz&xvTmZ2ve zw{`1tU|_UGw1I=_P3LFIaxIUz54UY21Zve*qInc7(ITyMn@(R@mz|K<3DMe4O#klKyLnO&?on*RL`oFN( z{38H+;|+&M0+b{QtLpoRZ5yXlQmQQAW@#>c`(sRu6?e;1O{#5#vLI8(Q-uZXWqf4| z1LVB?;A=sZ;Kv&rldFr5c7G$#z6@ZAQt49q3Csrx5=Z3Xgy$TZ2KBHRl#r2`rC67M zcAT9h%q~hpt=Gi0;o3Z8j8@50s#mG6E*`}Sjd?RTiYmsB(4EePFX+Sux$W6z;(LT!=fCLGOe(fO5=`#~x&>DyrK6P>XCFnr1WntoJz*$~uDUo?4Qt9XiVYYk+zyOXj+eLl*!0 zdp;@x_KCeCCqO6r5?g?5a+4YHNi& z?SjRz&-2ZrsW%;7@XHURSadYq-tBgRTuoHX&M^xG4LoES6SXDk7qsE?Z*^WBlJRg} zx5%k1=ajHb@?NNkvBx`2!`L>F{yeSFs zt))E7B!Ff&iB+P(d@3JfwYXj0^Iq*-Jpvs|uRqtp?H~RHsd3+Ie&17%sH1tx4K;V0GsmQM}EN(IEA0pXQ97GlqjPSVq=t@%#aNfQ) zpY^uACl$#2o`G=L5Lc_D_4WnPsTa^!YaC%BcdC#iBHkY83XSJZ6H8-S!qSMT5~uhe z#)q;K@i{rGS;u)r)xARkoR5_3^>ucXl@%}65t@nC#>PsV>VoKmWgSbMG7{KopWy>gDj?+g*e^}aJ11-iG4kc9=!cbrAp@(L)Ee^Q|xtr=b<+| zJ|qNWmX;1oGhzDgpaRQQJ_yAIhm&UYKIs8U9M3;Xd?zU6J?Ku+XlII^d^`1rpjGOdY zj&0YRbXlL;T~TAIN)NB=_V08VM#!^Pt=D1Cuc?E9!M5kWDs0qs-BmsD-4v;x#$oU; zASqRL@vj~Y#I)@l0F|~#O3|?t!U@h>QZNM;SC2=tR_2O=j0fT;y#>nNBU5DjCS{Eo z>7bn*hqSW>PAmDbZcQGaz=z=$lmR$OJWM~a{CH{YW>u{NAOHa{ zuaaaybfIhZiTn2skoAFRfy|k+=l$fpp4*wHK!+VK?(e_V(#pj47c41w;M-V)z}}Q^ zH13^VMcr^ufmhX@aLDhkok3b{#ygPqHQ?8O|Gqr)18&D_&O};&^zA>m_|7Tduu$)k z{uf$Klkzp>y(j{l1N{p*f8EpJfb<<$et+?w(D4kBIy2A&X`iLi@z6%KkZ9pkX}IF;Z}72_q*3F08Kn$drta$QvPX)d4TA1I8WGV z!+v}0eZqNgZ2hg#r++WN81Zwe@j0N;%Lu6}kX+7IYvMgI(+A_nL;rTgqi<&hAIB?3 zIsVpb{&a|+={4ptD_Jj|-=qJJ4>yA}I$!Yo69Y5%1p)g`&nHCYznjRq3N*o?+XW2W zKfZFDI-v8>?bJN_cN3bYTNkTR&A+diA3)*)cO|0#p!h%S#3=$zcIuS}(fzT(>%=(sNb$MZRS6ijH zvK@+y2n{WFf=~Jh$46|TaDysu1HCHCskxW36o_bxEEF|0aLS-Eq$Y8pGQRPZSi{KiuX?sw0ky|VQ3ZbZT>vH-)+L68%^o<%BRYlh-_eH&Pv8(eRc08_ z$nj3{iE86XFq0xG$@^wX6Fb0@UjT!yXNef#Ii3B_3`Zt~7S5kw#j!mo2)l!WgJXLE zm_m4hPY~=g&+0z=6Y{OAtLu}Hy#M@d_m(>#Tc!KOrE5WMCR*P}D)YKsACt`-RS;zz zU}-j{S*eaUe6Yh1@jL5+h2Ku0c0_-z3ZB%FMdn>Fr0p70FfdB63tq3+wD|5p z5(>8FGRYTvuw@(Q_YbLAMLCg;jjI#!i~FmCyO>e%S2%C%J-HCZa2dFSi}f3A)5=M2 zJu@2 za`};+T`hkqqfXTwTrQZ|z@B{F{o<&gqVf%boA%%B2A|xo!)#S9uHNquck|Kk!Tvxj zF62ap9FX4{IwQZghQMXnVy0b+D2~b{EOIOy53Y}PZKvLq$(7MA)a5Da_0Z+u;3R^I z5?rljemF#0BPp49!pV%-y0QwAJLB&iPM)%b$-FG^U8B8r=!Bt114JY+SnJmx_9XnWi(3gWKHCk^jrx|ZJ0Z%$HI|Q*k za1z;CL?L^0HkCrZej%uxNSdW#^;eL2$@wqKxSo6a-1U{Z1R!GCmPEC|E1#~P#TREj zX*<=FH{8p*15(LlBuOkqEZ_O_B-TxA_A-bljjEbe*12w(zH4|-We-^F*!8W{-aIaknc&=h38}jY7|HsZfUU4?wSSiQJ7&6yd{gla(0zQ@* zjQ=RjB1*D)ezLG+y)sAR1q|mMSOtpxOC9 zJd)=f}?K5JB_ho}Q8Xxo|#0O9~y=gl!6e_3zm)`F+% zlAb7_QJ-#_syD-Ybi&pd z`BTLrFYA-a0HT8P03Q0sH{><;s`02>Xb*S_ZxpX`GJ6s1fXdVBaQ0_u6A9B#77D>K zW#+GuQ>y2vZg4?7u^(nv|L~T|>Ds*#Niq6Mn*Oo31nbLD<#hPilj&FbUGJ!7qHgxw zw`5+7m9j{s>p_;Aq)$~CkH4WaM|)Rq`%BTZ&v*u6J`uh~Cr2nyuTfr2^h&%a>9O%C zUl8v&_roEl-u%l{At%Hf%Q>2#-46-2NRGxp2XUg$b=V*j&QbCGO9#mX72^l^a8p^1 z76nPjF|>Ij6Z4nY+q$?Kyh#~=VUeJ8F0??E*apGVFdO(>lR539lLPj4vE zp-0a8@R1A%G8*(Y7Mo^V&X5S`{CnMJ2{LE=VrYRK8(RfDh`#)ipIWrnn|C!bl@I5; zvo!&@{*D4J;D@xNmLiP^xp!%Wzo}8oe%qIh!qN)rxTdL`SO{fG&@(G9@za}4_;?%= z$nSJVg@co`?5^&qpLbaOw$mn>^GLw)Zvf%nvuz>$v``wbC3pZDHsba2x@O-z!=m#ZuQEE_( zTMDHr;fS4F{(799j~Cx)n=q7lH|3&orTG^z`J{;cOs);P2|xrCmfT(UF0X>N^L zuhoDjMmsj=3${g0>2nQAMN%x8Fa71uj73wAX(S3!hS9Llr}a8i-QEWZ295QLF{o^q zXO*7%3Y#5Z=C_ol`DS+~(W`iwB7_9LqD9~%Z*^_g@6&g4iVi>uyc*`~IjZ}cw$E^4 z4U!DgGcKLN#Y&~?GN6Gi<^C>v#G-#>6^glT>4tU^l{TChvHZa zj`3@yDND-2_7nY}ki*qSKN3krH>?a2Ovl2mG>5-{TFIn&Pe^)fV(9yMpdx~plD103 zCVmwS?1v{0_gX*lJfN1RyTYht(~aYN4n6YmbU&%VT8!It%lj}_@NpL9DliLFo)9fQ z#@lYpVE*vHWoNRG+rCmk7k{*y*I58s;E%9Vr=sT$yHPjZQh13_?`m2S*)NdAz9p3R z6umlyP%fCXg6g38H5f4N^y4-oUc&S`9L?as1lu#+`s-;@BXUTNZJ&DNDcnIu4 zch)6fRk^N=yhR7omE6dxj#k=Nx4M1c6jhxCOKi=`nL|tsP<5U7{fImL5{Y~wBV1S0 z&(=t2C^L~ibRW3J8(ro8C`O*2(`4<-7m+V{$Rp~lvgrh#m*sXv8FA_}TL$Xj?8|4) z2n;Jcd!p$%QF;&u(lA0UqY2KQkh)=~2Ea*|iys`+ALl)Lbg+eTReagrP!FSDTVKR4 zFF}J>?M#VRXqgLwnjbi+jx{2UW}gwUwEKYoa7ZsnlMEL8Op{Ae2 zR-_iV`B%+v3fUA7s@}xis-fy$neG7})8eW>fBAAT)+{O(wmwp~*qJ*N&02P7bdcCP`=FK5ANDT=X=Z$SJ%>FAu|ZQoHV{Eje8Gkkn*} z;3-Vs9FhYW$6cp#iwW)|JF$oS9f$(z4ecDc{jhj`J9itrR25BmTZHR6C#lNqiE zng$2&xOkpUBvB!oDHe>^BBRAjqd(dHNUY}@9g_H&F#JCw3VwBAqOP01zm9TID&1L zlp3z81jWnU!VavQP!mzQM-WVSpAn8H z2f1>3ac2P+xoO6Yd_yztz0}tW_6X2dXOiJldX_+fzV>ixx--D&!lAA&%uW)+=px5l zOA44qe_HxW*8t(%ACTzjf3*E!Kz|vg^C=y5J9#ep4`K!=XioqVM)79;kL>Z6Vb%i7 zB&`mT&G2u=#thJ{b0rRcOyNJg?eY}RME{iJ5aqwQSs1Vq`j0&ZoPs~Co^nFq>KStx zr|{C7m!+Cy0AGhT`qr)gP`x{xr@l@H&DgWdD;ok=xLWV{Hes)v`Z`HW6ov2pUSk4a z>@4e&SN@}2U=q>X1&p0HwGkix&DqU>OX<}s{_yvgfBm%l25==bgWnzjeCQvoJm!=t zb{l!e{m=KLQd$8PUJ+B6ar<9HwMiFfqKwA+-&z2-EWlG+omnaV?Y{K_q&Bg_ko;eN z&HN$Iaoky)78i&vJgbvJF?ugVe2$j&6!6xEy~G3`pmFG6rB;Ao``x=HZ~eWLogVmg zb4S1U!Uh?|&|6W$sP3pbTU;Cq8|x`bpW(x%u@3+l$ol<>lMbP+dL2Q#DHc1JWKr|M zY7A0=2j-kBb}|RP`Km4ZCpJRzO^{dligZqXzO}uuOX7+qOuigeXeEnm%-Rorrq8BQ z{Sv_0juv*)(OS^F1OQK|)X_=yEDra)qYjhRP90aXQ=DlN zYnf3@CMC)qw4LisKwf*B{$wQ^~y@ZLGzV&rda#vKuc^_9;Mpj1W zr`vZIkYHS)*nWq@RuvR77u2c5v$p0dhT|xq63LLGGrKGbUF8OOE0c*!T zO7yvH-Hs~|YKhRcu-M%u4jpr%WDvQpcD(bjZWE0t@8n+ z`~Res9y-kaF^ZIAOk{$+TpG6ZfGCnx&0o7K`enU!YW>6Iidr7G-p2fH>l$2T-QshL z&kIZC(O48B+N!7?BysrGuiM}OTg*(37aN3o77#PIWLjkgpgJ=mfI>04>BT^iB;sEC z3UJlj5gQSkqLmZsKTmuXKN#Yt1REb5aL`3c_L>f2M&tL)h99$X7ktZzS-%$7l(S*1 z8*HS?yDy$1c?8Pmt*HPboa!1iPF5;@Q&gODnmaFMb68C~UsoIaZS0o_b(s~I`RW5# zG>OlW(BFIh=3P*Jkh18mIuM$I0#9L&BkKY}vl%SLwaApS{tE?FW~YH%-V?`pn^HK* zJHSoB7K@zPRDwV~Wx%J3NJ!ukg6x2;Q>tBCEXVcB%yNQ|>CofVk<2!wP%<=n>`Y0aVNo5~zMu`ZTN%Y!E+dH!klQp*k6wRU zkgS2*Q)aIJW50nMJ4;*n`Zo5iZmg1hTOyQAd6IW$Yx!x$Gs_lzvzC2|G&b&hO?~aO z2bmsRC^*Tj;I!ILjxUP(=}ZXolRiyrl$zo#J!_hY&nYQdTFwp>!p@_aSOTMY%O&G# zG}?P3%+MCaZpViXSmRDMSK}HhXkn>2_D)WA_NNt15ESiM)2M%99={KdRN}2ymJPmP zvo(segmLxbf9-{SE2moT@6C1gj{R8BMn$LjT_8{u+y9Zj*#Mb;%VJ7II^>r4EwMUt zu)002DU}%A^43sJS2xz+ar&ui#zf0ov+0=m7UD2#A9(`$kLT6;Mg!4$k9iEpY&|Pl zAM!DH!1D06dSSA=e}iejWXM3?7dKf?z!Af-W07O z4eYNMmq@0+HMv?GZZ++hr*ei~USvY#G-0+|jg6SS%l|ZQ(4Z*EA(-otANcX$V`3A* z{i^8ox`R-VDDqgvz~iT}6JixEm_2+&|JQCVwSNi^AWZ7gB$w?k_i1sf#TXU@#42W= z)k}z#8?A8CDMP(aQ!P0(v95_aoq2Ick>9>(Lc~(Fp+t@gdPp=eC#x1%FOB`vB6117!ysa0gJ6*cQINy^0n!(G{a9(CIx@d z2DGQC`nN23)kQh~laWpRjX6NM+)c7+4C!Lv6y1t_ii8e+Bvzn3&w&@Sr`!k}vTK0y z4Rq!}h(;$Wo$`*iQ*lJR*k3yNEVW-5*yJeVyCyZTJ7;sJa6Vb{ei1;)^a%6moBSxx z)CSSrioM|jJtqHR*$s~0~a7qs^x4YOXXh1vw_Zt^D##T>1E?JqG6zd`XbqPxKr@Y*r~ zuZ?VZH^e=$3$uoDJ}U%kbfW;;z z7mu5`BTk`1%fumo38lO#cIS}Luca&?P>EN?Q=6MDhZq>QedXM&Aj182g)ZZXKKFxV zEI$Ew>M{AseNg<90+kfEv1!jJ8*TDeOAf~WS9{+X)ztESeMP_mTnix5M5zMOivm&< zkRBum7`jNW(t>mm6_wt53%x777tu@ay+);%fRrRs0{;n?pWM&yTJM+32UgYuCUfRY zX7+xby`Q71lw_m1RQoHXHF=xnQ5q~;M$YVRIQKMSXRT^0KFmUsdGgV{4_9z?TaG;h z3*J4-9fL!{CzozYx_+=dSSs=^C8_Up$8uHurZuBTlc>XF@uK;DoR8zm-c-zPm%B?4 z8Lu~98un;iv>)Bah>et5$x}k%Z0FoJiu6nut!^TFYn^SVR;MkJXtB=#>1A=ptO*BC zT)ygdm^mWo(xijHib#C-WtdPOj`?hV;O7%sEH@{y5=C1c(ls4ltsX;t}DU1r+hB>(v893raVB8+|904t& zGR1`o3JUN@`MPt(>|r`PT_^_gt)qZ1=2DEDcrEql+)_nQ`?BxF#H2S1IVq;?v%`{Z zx>^@mj*zBIgOr<}F7@r}ssQ^R)FWOU2|?lndIwmOdNgQO7;To%ZQ9P=%sPxNtofwe zY;DFc5o%oD*sE6hL|sE;fUxb$a9Zll)NTvvsdQ1tkA#y78-UJEpT9xGZz2q!Ue#aPBXnc-rC2~=!ik&NoJD~@bQEs#N5bH$O${2d>zOf_|{MS z8d~XXpXFuC98o=XHcp75tBGs4P>5ksn>qXC6NDcqd!>pr6s<(OmSY|~(0F~oaP4$Ip`T}e%FEM>RO4n6 z>vSaoadFEf=di0~Ava08q*7Q;FAl2E_x;b83z9g=?%P|c>MYOeGQ%=-3D2L_k;)R} zbr!G|hWy$dlFu$KBxUNR0jWur9ck`y{miX1=T}9Ro{%2LGdTdysEP$y{+6^KAr%Nf zE)Y?=<60yS4uF#eg3`wS05c$R91eO2|GVXn=p`6VG5|z7r8C(-#o9mr<1;hpA-x#{ zpMQ-QylTwC!z9$L|Azbjbaok;dnMSkh%xjOsr}c9f82BzV90P`asW52hv*_X`E1xm zbQ{;mGE35+9Mm3{Lf@UJnQ?%w57LH_>Xp}t!g?$Jq5 zn}dov!+0_Z)no~qVC;&Jl*5E`2yl@pA(LL&$)!r6vQtZ4CWk~I8^K@fq3mO)ta^A}|5$(81rdv|KyJCM+l75F? z)bItW#&h1&;rr+psrQ8qb>B&;Z|UyFV_#>6&#{?c#Ow$=0$-@Ud*>!RId0Hn(KNMt zAmWiO<4__d--1arcCke+xkX)QMjAxli z3#)>vLXny-vxgs8-6ajmJL;co^Ebxx^0U~YjTs^Vrk7q zcbK4GQDW@We2rB$=KU#f1V&x2G#pkJ)dsY1vs0?`Z6Q9*tH|7W`|5fvNiILU?dl`q zFQ%Wi&CBGyp_M?swCd?TM>$t)GzA^ACSXtz71elW79A0<)-W)CGT(Lt0yHY}kJM5V z_V-IKpc z({v*1Hx#!RiOc9nqC-v0h3YOS$Vz+Gl^Nza3GWHpOk9@>ic(w|riObzk0r=UlZoT+ zl%VHwQqrGb&~DJ9dGc~lwMZ?w=!#=5r~tjSVpLB!3agw7c!(Na75=8IpbUc7hfg>9 zp2>J~bVZ-RxhI}MoSeJY&URm4uVHH3U__)08yRthziAp&`9dB7$}lgSpmE|ih_W7gKUOcnl_lFD`M|9=h;GIG&U%iDl8S&qJ6(nAa!tKf15M-{ z5q^J>2ImLg;Fut#&qK;y_jJwpP*HkV--Y}>=@tc}y9n#i;%V|X#IIdV(+l+`;KHMA zygoCbX>yhc?z2xVzM-3%Pob{MKapd)S*wdgUCdxLCt*(19~Z%{E^4dAOw*Uy zl?UG9BVDQur>{>Nq{D1D#G#g%H|B};MGt~5F=Q!Lmm!xX8~KFJFr$*;WniHm^zmFK zP{?`#=N-tJ>W?j=?4AYsWE~hCK3-v@0d_h~_ar#ed0zriXUFDwXDCeVTAlkgnr>C) zvS+IKvx+`KKT*+v0;O{b5amLxcdN5!VD)f|x|U|~^K0m4E(6I2E4TnQQEc5cIHD+>(@(x>`ERLSZLvw+|1#ine(IJDp;Rvt3MfDo7xMEycQAW|q za`O1j-IG=0El-cY#d#3`>RMq?5W1Dgc_sn>Qes1Eyk0L}{>mh)Jw1{-XEUxEzR0@H z<-zu}o>;7n8hy%LzXKgtlv8A&35Wh2Mfa}BTF+TDm&s>dqI}A9+|tM>%t;u#x3vAq zTme)B*%EW!OlM|GM)f!ec=9Gtpd@U8+-G=uaL};axFjUhYw3%S#bJ6}-xsNV8y>7X z@#f0*NP%?BZ^02@2RM!d5P2Vh+T{vzgqOT1_GbR=^SsN88{d>8S+ADdf#jiH+5D(4N#DOELF5KxyL6q zT>oOm>L!CH57D%`TThzpO0Wbmb4Xl&O99SD^cGH&yi6@^&GbE@IfDffy}D%k%>%sT?v zLvY!FBBgMRuHFyM8lN<1wc2M%xGS)E^tU{urrlhkx)9jCz$?O<#As^k=x!cs6OfD9W|_-KT5fCb4Rt`##7$+N5N0@>n*~uC?kv z9%smx7pKMFtcfajKi70AvnsEDRG^k?bvJ}bhKx%s)q94zr%>|(=&#g>ylCQn6#=`l zcu{S-hv&1|JtxN*vZDv=^lBF_Zl&zxcit$8QGAm>&GHj!3bA=`unChxD zb7yiho~ZhGe!Tm+G8$<+Vy7s{gJwD^R8jET6Ps4%H3-t%2;HkJ#=APwxp8q%;cgG% zD|8}GtA^5VH=Kfkw3m#+A|o}DHb=T65)zCxQTf?*d$o18%PV=ot^p|z5^ZC8Tz#__ z?&7a$U;!nxU8klRiLk8D*;~oWmpFXadc&|}n((QD!W*dC7tgb1=CMAZosC;NcY{T2 z6Hk!fK{OfAzJ|Ux&TE-Ac+x(n5wU4 z)_XnnjN2pof?qOGbc?dfxPe|N)glJl5Ur8D9KMmFyxSOj*p*8Ur1dkfyr>`0Jmmtx zN~a*QxF?x!?6nb%hoz%ILlfnK41m>F&jR%YB85Dg;79PWkK?}e+868Y!O0Q3?s7-G znI`2ufmt+xm9;{`Z$7<3|{JWk;lLEM*0L`m^ClRP2_2%ur$v8;an_Be$LB?rVV zf#pXS`uAIEKy^VSq<2U5Pj2y-Pb*+OvfW`}$6K@U+%T>%h4C1bk(vcl#%QMSFTu(n z-u$4Vb^pRK+H%za%uy&v4j*r|#Ls#rXEu;h{2m%OqyD!~{@E%-@J3CYoTJ~+N586$ z#-lKrykdP~jfK|r&(U6eO9&$Di0%}QKYM&8m5v`Qi-O4URto&}s+rxVJU%Qaez0Nb zbjMrY;s={8kbU;}VA;V9l~;iNeY{l`za|E$9}pcQkgp@a4NYfeKVC)YH6!Ta`Z8(m zzt#jWPw+s)BK{Qu2nAhGI1_99YfZ3#8_KTGe!LZd2mEhEeRKbqNASG}1%40z2n~+DJl#&{>y1@L4S%bHH`drh{rRVTqAkeu_%QTf-`w?fDHIsbSJwWn*NPBXtIQ zL8aZTA##T+R8SafPWv7dikS46vEc_rOX6KRFkGAahwhq?q}vt-Nc3D$8_+kbV%j#< zi}MV#YK2!Q*{8K`B#JqQGI;FCIqT2dY}kuj#nc;B0NQ{O#2fL7p|Q*LNqtn|ZNe7dCYdXVGtIa&|+KO;~fm-r4-&gVrL5WtYdF!&E zUdU`xO+EX${i3~)4C^ERCn4NoDNAWK>u{G`#G!IK;qj_DH9<2mS#(XqzO#ao0-5l2v66xSS|P|Q zvMby@`)Ji-H(c)L_YaZsii%+`UdX%&R^p{tYnnEpGT^oeTZ9XRHMs> zR&+%NYCVsz>q@q{7dsukU8?EO_cFS5WPLa4_I?M0lG=v;PH^%;O_$41^+e5eGq_ZIAe4; z$^JrD&ikVyVq&i1_At@Drs*ZKy?2%dYKqdCvnq?XdGGuKlohkBp->U{eoXB_kCw?m zl!{Py9zT*INmPTI#ZSq2TFj9}AT7{owYLvK$t4D3-AzNXYR1){sr}oGCWkoIXRAA6 z?n?SA&3A!0J%j85vuIe68%-CfctuqOP_!zP?H~g>ib9q$$xPMzb(ZQ|S-7x14C2P# znR>$F55JA>+h2U@Ii+C`VP&Ro7OYbLou68Kd6H-3Ia@LCKY}AQpCHzz*!b%xiAhHg z7qqlN{w)R-i7%8VL;lUAyv=&JT6Xhmz^W`%K(QkZK18}S_{mi?{7Nz$P3n4}>K)jx zdB5P*S>U~jGcIipIm!AI=w|doKy@Qvn})`UcxG-d_vwUnZ4-%0p!%0Ki(~t#^DGuW zKQiT*2cK1y%opN@U4Be3!{LhS@@{P*kvzL9vEUL$O_YB+7NhpS`XKF**Rm2Ya)yEU zA?^2}XtqflPC0}oImECbP`BbyBHgV@$$Z9o-WiUuY8!v+@7A6V7ld5qE2ui7?It~o z7PDs=4tp4^pSkpX^w02%joTR%I22YPe&c)2stm*TlmEGt~e z5OMRPewMl)q-n*Of56dYot?Da6zG`%1gIv|j_0WwvDIaA$F4=4z-?O3VOF^!28OaS zB6oBjSk^b%oOxqd#IlW0qGeU7YGrAOZ4T#-mwC6K&~6ax24Ck=>Wp}?pue9-ztcBv zH(5JZy0=LGgrk>ON^2d_5Ltf-@U=WfP8hQY~e#T_C1rF z_}sKkWsAB5|Blkt3Bs#IbvwCa+l5tMwZ~wUN}BoltW17$O2LIwb7fX-5izJeAbfHU2z^jAt4kFxnqQGd56@qD#^JcGXZ~)aO5MyNGKrf_H8Jd* zq?dsNucW>@rvS@_9}-tVGv0GdM60DM&b-L z*9XKNII!LtjFj*CE6Og6*uy{C!*cCq?cl+w-IFD}D(M8`vgXkPkl{iD`t9q&SQut~ zpr?NLnReD*X_qA@i)mjPvjDWP2wnH2a{I|QI^iFupHfPp6wiI>M9%@}#U_2FB?Uit z?bfhZn50*!bdZ4GrOpjZQ*YT-ewj!qpPhNj6zbSf%lUM>iK?jpOhW9mmi2t*il=@Y zaZJVvN!Q@ZhssncwKA{b{<3M?{IuDhcgXa3v!y+7#H$SoPLIM)PqQFp0j!k@zxqk% zkJdpj^Njj*l9#u4w%1KQz6<4_>(3D?_N@LkLmV7IFX^T0wb|-De7&fi$}Jh`R>GeF z;o1h1yWKowO4My{nu}o7t2J}?qHcW3yJ&t22U_~hY{F? z5r)>KQ(=dDvj)mZ*lzE+N~3U+{E2t>HoLMT3VEyCrWD6)o=d8@cM87Lm@k9O(b)uz z=vUXbab9Afs+>i%nZA!u-n&JJ`&|EcA;_=`4xS7k`EFgX!TIC519{_+(9qWRHu8Q} zV;@?*)p=tW>|H|Ubmh`+youqV0G4*K3<`yQh-1vXvusFCr_XDPz_sxQFMU&LXAsdg zjfqrEKIl+~ny-Zpuov{~BPH7-2Tbv`H!fKGbHOn=uU?KICy=^I%wc;q;Eks#$HKD{ zuAqL(Lri>5yz4w4t@0OA^9@@3ZjvGbRNht)+}2Z+@zxjB0X2aPb;;ALS=Upy-<8^uFMX!{*)RnJa|{hW<#55ryq8j7DZ)&DaPu}RM zsQU)kh?OVayq8$~bR%8K=4z3;gIw$a(b8*UloPVT%Md)K4aJa@ZNK=2jUjJix0?s4 zH{&eWusD*{kf@*Mqkq++vBGr9a!cER291L#spQ$qGkB&N{I$y8bRa*Fl{Z|- z&*W#6)FRj(^n#e5;nq8(GLrt{PtIy3`W_##LmLy zdu+v7>^>Xz(MB9@=fgg|BkEE;*RsEX6z4$h?z*J=!^G`3Wuu2~31!&?>6%9Mj-->6 z?r*+No;Y_X#2-tw?!8^;v^FWb*0A2WwYiB*{#*Dam{gJp*^@BYlqn&vr@i~ z;BZuO2|QSr*qw80#mTry|-3EP<56T4-1gvBUAiQeoip_m+Ghl`C3?AywaM9&}|Om za9bz2#O29hlc;36XwvtcCe+@(6E-ON(kI)bRNq&zFIjfu8&lGDc(s}1D{O7In+Mf9 z#T5e;a^B=L4V36Cw5C3u)(jy{qVOS0gu67oAnB5>?9mHU_aA9|^b!Vh%yEUJsw)cm zJ1#obc69UvC5g@jZ}Zh1EDdO&4+>6YrYz4e$1w@X9(pbLSG0b{s%*LrIoV&Y3@6hGVu=OIe?jGOn z%q;HH7jdwd3lLFc7al&=q!?s?s+{mF*JV!P(Xz;N6*hCI@p2?`Rk7G?Lj=#2sL-y| zer%Esb=|(3@-1qS%AB_0DD$VC2^deus+Kn14Dj(qW8w2exP4-b`HeuCBkjjkyn+H%)F{r*9;ja$Y{9pI@S7wBQqj^DQ#=+Q= ze}3}SUIgK4t3Dl*J6Eo{1?jQ34COGDX2CaqMmT;Bx3?JIQSPO+F)P@VqSc?5e07*e za8w{P>0>;_%B9ld$}V7npqbHF5qCdx<_uS++P@4Luboc>ZgAw~GE4A<8uAPyNss@x jcF?nbp0YtA+73^N%*#-oOk%zM7x+<-g~$}%HGKYm14}2O literal 0 HcmV?d00001 From 4dfe8b394ca74fd0793f0c633461a21149a514e7 Mon Sep 17 00:00:00 2001 From: Ahmad Jalil Date: Sun, 5 Jul 2026 18:09:01 -0700 Subject: [PATCH 11/12] feat(demo): add GitHub Pages T1 chat demo --- .github/workflows/pages.yml | 64 ++ .gitignore | 1 + apps/t1code-demo/Cargo.lock | 731 ++++++++++++++ apps/t1code-demo/Cargo.toml | 10 + apps/t1code-demo/index.html | 551 +++++++++++ apps/t1code-demo/src/main.rs | 1730 ++++++++++++++++++++++++++++++++++ apps/tui/package.json | 4 +- apps/tui/src/ui.tsx | 1057 +++++++++++---------- 8 files changed, 3672 insertions(+), 476 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 apps/t1code-demo/Cargo.lock create mode 100644 apps/t1code-demo/Cargo.toml create mode 100644 apps/t1code-demo/index.html create mode 100644 apps/t1code-demo/src/main.rs diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 00000000..e6470668 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,64 @@ +name: Deploy T1 Chat demo + +on: + push: + branches: + - main + paths: + - "apps/t1code-demo/**" + - ".github/workflows/pages.yml" + workflow_dispatch: + +permissions: + contents: read + id-token: write + pages: write + +concurrency: + group: github-pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + - name: Cache Rust builds + uses: Swatinem/rust-cache@v2 + with: + workspaces: apps/t1code-demo -> target + + - name: Install Trunk + run: cargo install trunk --version 0.21.14 --locked + + - name: Build demo + working-directory: apps/t1code-demo + run: trunk build --release --public-url "/${{ github.event.repository.name }}/" + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: apps/t1code-demo/dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 471855de..9e08078b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules *.log *.tsbuildinfo apps/*/dist +apps/*/target .astro packages/*/dist .env diff --git a/apps/t1code-demo/Cargo.lock b/apps/t1code-demo/Cargo.lock new file mode 100644 index 00000000..55e87612 --- /dev/null +++ b/apps/t1code-demo/Cargo.lock @@ -0,0 +1,731 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "beamterm-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0375f7f3df163a52a06807f2150bac9a5da1933864ed40d52dda7dcd7cfa193b" +dependencies = [ + "beamterm-data", + "beamterm-unicode", + "bitflags", + "compact_str", + "glow", + "lru 0.16.4", + "rustc-hash", + "thiserror", + "unicode-width", +] + +[[package]] +name = "beamterm-data" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f21ff1fc630079352bae9b024f85519bf1f641cf7f326623f4c0b59f7ea834fd" +dependencies = [ + "compact_str", + "miniz_oxide", + "thiserror", +] + +[[package]] +name = "beamterm-renderer" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8db76d04d575a0534ad5dba63acc05ccbe69597f977f11c59a612cbea199112" +dependencies = [ + "beamterm-core", + "beamterm-data", + "bitflags", + "compact_str", + "console_error_panic_hook", + "glow", + "js-sys", + "thiserror", + "unicode-width", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "beamterm-unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f44155933cdd0625a699c554c504aab30a571e4df68f3532fb95bf9cf53b935" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + +[[package]] +name = "line-clipping" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags", +] + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "ratatui" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" +dependencies = [ + "bitflags", + "compact_str", + "critical-section", + "hashbrown 0.17.1", + "itertools", + "kasuari", + "lru 0.18.0", + "strum", + "thiserror", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "ratzilla" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53309a50b2cfb9828d1ea653b77fc6d2fe08137b05e65aa070ab87630e35b49" +dependencies = [ + "beamterm-renderer", + "bitvec", + "compact_str", + "console_error_panic_hook", + "ratatui", + "thiserror", + "unicode-width", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc-std-workspace-core" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9c45b374136f52f2d6311062c7146bff20fec063c3f5d46a410bd937746955" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "t1code-demo" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "critical-section", + "ratzilla", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +dependencies = [ + "rustc-std-workspace-core", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] diff --git a/apps/t1code-demo/Cargo.toml b/apps/t1code-demo/Cargo.toml new file mode 100644 index 00000000..b454de70 --- /dev/null +++ b/apps/t1code-demo/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "t1code-demo" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +console_error_panic_hook = "0.1.7" +critical-section = { version = "1.2", features = ["std"] } +ratzilla = "0.3.1" diff --git a/apps/t1code-demo/index.html b/apps/t1code-demo/index.html new file mode 100644 index 00000000..0a0242a6 --- /dev/null +++ b/apps/t1code-demo/index.html @@ -0,0 +1,551 @@ + + + + + + + + T1Code Ratzilla Demo + + + + + + + + diff --git a/apps/t1code-demo/src/main.rs b/apps/t1code-demo/src/main.rs new file mode 100644 index 00000000..f2783a99 --- /dev/null +++ b/apps/t1code-demo/src/main.rs @@ -0,0 +1,1730 @@ +use std::{ + cell::{Cell, RefCell}, + io, + rc::Rc, +}; + +use ratzilla::{ + backend::webgl2::{WebGl2Backend, WebGl2BackendOptions}, + event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind}, + ratatui::{ + layout::{Alignment, Constraint, Layout, Rect}, + prelude::{Color, Frame, Line, Modifier, Span, Style, Text, Widget}, + widgets::{Block, BorderType, Borders, Clear, List, ListItem, Padding, Paragraph, Wrap}, + Terminal, + }, + WebRenderer, +}; + +const SIDEBAR_WIDTH: u16 = 28; +const COMPOSER_HEIGHT: u16 = 7; +const FOOTER_HEIGHT: u16 = 0; +const MAX_VISIBLE_PROMPTS: usize = 4; +const WELCOME_CARD_ROW: u16 = 7; +const WELCOME_CARD_GAP: u16 = 1; +const WELCOME_CARDS: &[(&str, u16)] = &[ + (" Create", 11), + (" Explore", 12), + (" Code", 10), + (" Learn", 11), +]; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum FocusArea { + Threads, + Timeline, + Composer, + Controls, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum ThinkingMode { + Fast, + Think, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum MainView { + Thread, + Settings, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum ThemeChoice { + Light, + System, + Dark, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum ModelChoice { + Gpt54, + Gpt5Mini, + Claude, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Popover { + None, + Model, + Thinking, + Settings, + SidebarSettings, + QuickAction(usize), +} + +#[derive(Clone, Copy)] +struct DemoPalette { + accent: Color, + new_chat_bg: Color, + composer_border: Color, + send_active: Color, + control_active: Color, + text: Color, + muted: Color, + subtle: Color, + divider: Color, + page_bg: Color, + sidebar_bg: Color, + panel_bg: Color, + surface: Color, + surface_alt: Color, +} + +const DARK_PALETTE: DemoPalette = DemoPalette { + accent: Color::Rgb(227, 63, 134), + new_chat_bg: Color::Rgb(162, 59, 103), + composer_border: Color::Rgb(163, 0, 76), + send_active: Color::Rgb(163, 0, 76), + control_active: Color::Rgb(57, 37, 56), + text: Color::Rgb(249, 248, 251), + muted: Color::Rgb(180, 159, 181), + subtle: Color::Rgb(134, 117, 135), + divider: Color::Rgb(57, 37, 56), + page_bg: Color::Rgb(31, 21, 29), + sidebar_bg: Color::Rgb(24, 15, 23), + panel_bg: Color::Rgb(31, 21, 29), + surface: Color::Rgb(39, 25, 37), + surface_alt: Color::Rgb(46, 31, 44), +}; + +const LIGHT_PALETTE: DemoPalette = DemoPalette { + accent: Color::Rgb(202, 2, 119), + new_chat_bg: Color::Rgb(162, 59, 103), + composer_border: Color::Rgb(227, 63, 134), + send_active: Color::Rgb(227, 63, 134), + control_active: Color::Rgb(230, 204, 233), + text: Color::Rgb(80, 24, 84), + muted: Color::Rgb(122, 63, 126), + subtle: Color::Rgb(154, 107, 158), + divider: Color::Rgb(224, 184, 220), + page_bg: Color::Rgb(242, 225, 244), + sidebar_bg: Color::Rgb(234, 208, 239), + panel_bg: Color::Rgb(253, 247, 253), + surface: Color::Rgb(255, 255, 255), + surface_alt: Color::Rgb(245, 234, 246), +}; + +const BORING_DARK_PALETTE: DemoPalette = DemoPalette { + accent: Color::Rgb(173, 82, 115), + new_chat_bg: Color::Rgb(162, 59, 103), + composer_border: Color::Rgb(118, 55, 80), + send_active: Color::Rgb(118, 55, 80), + control_active: Color::Rgb(42, 42, 42), + text: Color::Rgb(230, 230, 230), + muted: Color::Rgb(176, 176, 176), + subtle: Color::Rgb(112, 112, 112), + divider: Color::Rgb(40, 40, 40), + page_bg: Color::Rgb(21, 21, 21), + sidebar_bg: Color::Rgb(26, 26, 26), + panel_bg: Color::Rgb(21, 21, 21), + surface: Color::Rgb(30, 30, 30), + surface_alt: Color::Rgb(34, 34, 34), +}; + +const BORING_LIGHT_PALETTE: DemoPalette = DemoPalette { + accent: Color::Rgb(173, 82, 115), + new_chat_bg: Color::Rgb(162, 59, 103), + composer_border: Color::Rgb(173, 82, 115), + send_active: Color::Rgb(173, 82, 115), + control_active: Color::Rgb(212, 212, 212), + text: Color::Rgb(23, 23, 23), + muted: Color::Rgb(86, 86, 86), + subtle: Color::Rgb(128, 128, 128), + divider: Color::Rgb(208, 208, 208), + page_bg: Color::Rgb(235, 235, 235), + sidebar_bg: Color::Rgb(224, 224, 224), + panel_bg: Color::Rgb(240, 240, 240), + surface: Color::Rgb(255, 255, 255), + surface_alt: Color::Rgb(232, 232, 232), +}; + +thread_local! { + static ACTIVE_PALETTE: Cell = const { Cell::new(DARK_PALETTE) }; +} + +struct DemoState { + sidebar_open: bool, + active_thread: Option, + focus: FocusArea, + composer: String, + sent_prompt: Option, + turn_count: usize, + temporary_chat: bool, + thinking_mode: ThinkingMode, + model_choice: ModelChoice, + main_view: MainView, + theme_choice: ThemeChoice, + boring_mode: bool, + profile_create_open: bool, + last_button_down: Option<(u16, u16)>, + popover: Popover, + viewport: Rect, +} + +impl Default for DemoState { + fn default() -> Self { + Self { + sidebar_open: true, + active_thread: None, + focus: FocusArea::Composer, + composer: String::new(), + sent_prompt: None, + turn_count: 0, + temporary_chat: false, + thinking_mode: ThinkingMode::Think, + model_choice: ModelChoice::Gpt54, + main_view: MainView::Thread, + theme_choice: ThemeChoice::System, + boring_mode: false, + profile_create_open: false, + last_button_down: None, + popover: Popover::None, + viewport: Rect::new(0, 0, 0, 0), + } + } +} + +impl DemoState { + fn handle_key(&mut self, key: KeyEvent) { + match key.code { + KeyCode::Char(c) if !key.ctrl && !key.alt && self.focus == FocusArea::Composer => { + self.composer.push(c); + } + KeyCode::Char('b') | KeyCode::Char('B') => self.sidebar_open = !self.sidebar_open, + KeyCode::Char('n') | KeyCode::Char('N') => self.start_new_chat(), + KeyCode::Char('t') | KeyCode::Char('T') => self.temporary_chat = !self.temporary_chat, + KeyCode::Char('m') | KeyCode::Char('M') => self.toggle_thinking_mode(), + KeyCode::Esc => { + self.profile_create_open = false; + self.popover = Popover::None; + if self.main_view == MainView::Settings { + self.main_view = MainView::Thread; + } + } + KeyCode::Tab => self.next_focus(), + KeyCode::Backspace => { + self.composer.pop(); + } + KeyCode::Enter => self.send_demo_turn(), + KeyCode::Up => self.select_relative_thread(-1), + KeyCode::Down => self.select_relative_thread(1), + KeyCode::Char(c) => { + if !key.ctrl && !key.alt { + self.focus = FocusArea::Composer; + self.composer.push(c); + } + } + _ => {} + } + } + + fn handle_mouse(&mut self, event: MouseEvent) { + match event.kind { + MouseEventKind::ButtonDown(MouseButton::Left) => { + self.last_button_down = Some((event.col, event.row)); + } + MouseEventKind::SingleClick(MouseButton::Left) => { + if self.last_button_down == Some((event.col, event.row)) { + self.last_button_down = None; + return; + } + } + _ => return, + } + + let layout = AppLayout::from_area(self.viewport, self.sidebar_open); + + if self.handle_popover_click(&layout, event.col, event.row) { + return; + } + + if let Some(sidebar) = layout.sidebar { + if contains(sidebar, event.col, event.row) { + self.focus = FocusArea::Threads; + self.handle_sidebar_click(sidebar, event.col, event.row); + return; + } + } + + if contains(layout.footer, event.col, event.row) { + self.handle_footer_click(event.col); + return; + } + + if contains(layout.header, event.col, event.row) { + self.handle_header_click(layout.header, event.col); + return; + } + + if self.main_view == MainView::Settings { + let settings = settings_view_area(&layout); + if contains(settings, event.col, event.row) { + self.handle_settings_view_click(settings, event.col, event.row); + } + return; + } + + if contains(layout.composer, event.col, event.row) { + self.handle_composer_click(layout.composer, event.col, event.row); + return; + } + + if contains(layout.content, event.col, event.row) { + self.focus = FocusArea::Timeline; + if self.active_thread.is_none() { + self.handle_welcome_click(timeline_inner(layout.content), event.col, event.row); + } + } + } + + fn handle_sidebar_click(&mut self, sidebar: Rect, col: u16, row: u16) { + if row >= sidebar.y.saturating_add(sidebar.height.saturating_sub(4)) { + self.focus = FocusArea::Controls; + if col >= sidebar.x.saturating_add(10) { + self.popover = Popover::SidebarSettings; + } + return; + } + + let inner_x = sidebar.x + 1; + if row == sidebar.y + 3 + && col >= inner_x + && col < sidebar.x + sidebar.width.saturating_sub(1) + { + self.start_new_chat(); + return; + } + + if row == sidebar.y + 5 { + self.composer.clear(); + self.focus = FocusArea::Composer; + return; + } + + if let Some(index) = thread_index_for_sidebar_row(row) { + self.active_thread = Some(index); + self.sent_prompt = None; + self.focus = FocusArea::Timeline; + } + } + + fn handle_footer_click(&mut self, col: u16) { + self.focus = FocusArea::Controls; + match col { + 0..=17 => self.sidebar_open = !self.sidebar_open, + 18..=35 => self.start_new_chat(), + 36..=54 => self.temporary_chat = !self.temporary_chat, + 55..=72 => self.toggle_thinking_mode(), + _ => {} + } + } + + fn handle_composer_click(&mut self, composer: Rect, col: u16, row: u16) { + self.focus = FocusArea::Composer; + let toolbar_y = composer.y.saturating_add(composer.height.saturating_sub(2)); + + if row >= toolbar_y.saturating_sub(1) { + self.focus = FocusArea::Controls; + if col >= composer.x.saturating_add(composer.width.saturating_sub(8)) { + self.popover = Popover::None; + self.send_demo_turn(); + } else if col < composer.x.saturating_add(16) { + self.popover = Popover::Model; + } else if col < composer.x.saturating_add(30) { + self.popover = Popover::Thinking; + } else { + self.popover = Popover::None; + } + return; + } + + self.popover = Popover::None; + } + + fn handle_header_click(&mut self, header: Rect, col: u16) { + self.focus = FocusArea::Controls; + if col >= header.x.saturating_add(header.width.saturating_sub(4)) { + self.popover = Popover::Settings; + } else if col >= header.x.saturating_add(header.width.saturating_sub(8)) { + self.temporary_chat = !self.temporary_chat; + self.popover = Popover::None; + } else { + self.popover = Popover::None; + } + } + + fn handle_settings_view_click(&mut self, area: Rect, _col: u16, row: u16) { + self.focus = FocusArea::Controls; + match row.saturating_sub(area.y) { + 5 => self.cycle_theme(), + 6 => self.boring_mode = !self.boring_mode, + 10 => self.temporary_chat = !self.temporary_chat, + 13 => self.cycle_model(), + 17 => self.main_view = MainView::Thread, + _ => {} + } + } + + fn handle_welcome_click(&mut self, content: Rect, col: u16, row: u16) { + for (index, rect) in welcome_card_rects(content) { + if contains(rect, col, row) { + self.focus = FocusArea::Controls; + self.popover = Popover::QuickAction(index); + return; + } + } + + let relative_row = row.saturating_sub(content.y); + let prompt_start = welcome_prompt_start(content.width); + if relative_row >= prompt_start + && relative_row < prompt_start + MAX_VISIBLE_PROMPTS as u16 * 2 + { + let prompt_index = ((relative_row - prompt_start) / 2) as usize; + if let Some(prompt) = PROMPTS.get(prompt_index) { + self.composer = (*prompt).to_string(); + self.focus = FocusArea::Composer; + self.popover = Popover::None; + } + } + } + + fn handle_popover_click(&mut self, layout: &AppLayout, col: u16, row: u16) -> bool { + let Some(rect) = popover_rect(layout, self.popover) else { + return false; + }; + + if !contains(rect, col, row) { + if self.popover != Popover::None { + if self.popover == Popover::SidebarSettings { + self.profile_create_open = false; + } + self.popover = Popover::None; + } + return false; + } + + let relative_popover_row = row.saturating_sub(rect.y); + + match self.popover { + Popover::Model => { + match relative_popover_row { + 1 => self.model_choice = ModelChoice::Gpt54, + 2 => self.model_choice = ModelChoice::Claude, + 3..=5 => {} + _ => {} + } + if matches!(relative_popover_row, 1..=5) { + self.popover = Popover::None; + } + } + Popover::Thinking => { + match relative_popover_row { + 2..=5 | 8 => self.thinking_mode = ThinkingMode::Think, + 9 => self.thinking_mode = ThinkingMode::Fast, + _ => {} + } + if matches!(relative_popover_row, 2..=5 | 8..=9) { + self.popover = Popover::None; + } + } + Popover::Settings => match relative_popover_row { + 1 => { + let rel_col = col.saturating_sub(rect.x); + self.theme_choice = match rel_col { + 0..=12 => ThemeChoice::Light, + 13..=15 => ThemeChoice::System, + _ => ThemeChoice::Dark, + }; + } + 2 | 3 => { + self.boring_mode = !self.boring_mode; + } + 4 | 5 => { + self.main_view = MainView::Settings; + self.popover = Popover::None; + self.focus = FocusArea::Controls; + } + _ => {} + }, + Popover::SidebarSettings => { + let relative_row = relative_popover_row; + if self.profile_create_open { + if relative_row == 5 { + self.profile_create_open = false; + self.popover = Popover::None; + } + } else if matches!(relative_row, 2 | 3) { + self.profile_create_open = true; + } + } + Popover::QuickAction(index) => { + if relative_popover_row >= 2 { + self.composer = quick_action_prompt(index).to_string(); + self.focus = FocusArea::Composer; + self.popover = Popover::None; + } + } + Popover::None => {} + } + + true + } + + fn start_new_chat(&mut self) { + self.main_view = MainView::Thread; + self.active_thread = None; + self.sent_prompt = None; + self.turn_count = 0; + self.composer.clear(); + self.focus = FocusArea::Composer; + } + + fn send_demo_turn(&mut self) { + let prompt = self.composer.trim().to_string(); + if prompt.is_empty() { + return; + } + + self.sent_prompt = Some(prompt); + self.turn_count += 1; + self.active_thread = None; + self.composer.clear(); + self.focus = FocusArea::Composer; + self.popover = Popover::None; + } + + fn select_relative_thread(&mut self, delta: i8) { + let current = self.active_thread.unwrap_or(0); + let next = if delta < 0 { + current.saturating_sub(1) + } else { + (current + 1).min(THREADS.len() - 1) + }; + self.active_thread = Some(next); + self.sent_prompt = None; + self.focus = FocusArea::Threads; + } + + fn toggle_thinking_mode(&mut self) { + self.thinking_mode = match self.thinking_mode { + ThinkingMode::Fast => ThinkingMode::Think, + ThinkingMode::Think => ThinkingMode::Fast, + }; + } + + fn cycle_model(&mut self) { + self.model_choice = match self.model_choice { + ModelChoice::Gpt54 => ModelChoice::Gpt5Mini, + ModelChoice::Gpt5Mini => ModelChoice::Claude, + ModelChoice::Claude => ModelChoice::Gpt54, + }; + } + + fn cycle_theme(&mut self) { + self.theme_choice = match self.theme_choice { + ThemeChoice::Light => ThemeChoice::System, + ThemeChoice::System => ThemeChoice::Dark, + ThemeChoice::Dark => ThemeChoice::Light, + }; + } + + fn model_label(&self) -> &'static str { + match self.model_choice { + ModelChoice::Gpt54 => "GPT-5.4", + ModelChoice::Gpt5Mini => "GPT-5 mini", + ModelChoice::Claude => "Claude", + } + } + + fn theme_label(&self) -> &'static str { + match self.theme_choice { + ThemeChoice::Light => "Light", + ThemeChoice::System => "System", + ThemeChoice::Dark => "Dark", + } + } + + fn palette(&self) -> DemoPalette { + if self.boring_mode { + return match self.theme_choice { + ThemeChoice::Light => BORING_LIGHT_PALETTE, + ThemeChoice::System | ThemeChoice::Dark => BORING_DARK_PALETTE, + }; + } + + match self.theme_choice { + ThemeChoice::Light => LIGHT_PALETTE, + ThemeChoice::System | ThemeChoice::Dark => DARK_PALETTE, + } + } + + fn next_focus(&mut self) { + self.focus = match self.focus { + FocusArea::Threads => FocusArea::Timeline, + FocusArea::Timeline => FocusArea::Composer, + FocusArea::Composer => FocusArea::Controls, + FocusArea::Controls => FocusArea::Threads, + }; + } +} + +struct ThreadItem { + title: &'static str, + group: &'static str, + preview: &'static str, +} + +struct AppLayout { + sidebar: Option, + header: Rect, + content: Rect, + composer: Rect, + footer: Rect, +} + +impl AppLayout { + fn from_area(area: Rect, sidebar_open: bool) -> Self { + let [app_area, footer] = + Layout::vertical([Constraint::Min(0), Constraint::Length(FOOTER_HEIGHT)]).areas(area); + let body_chunks = if sidebar_open && app_area.width > SIDEBAR_WIDTH + 42 { + Layout::horizontal([Constraint::Length(SIDEBAR_WIDTH), Constraint::Min(0)]) + .split(app_area) + } else { + Layout::horizontal([Constraint::Min(0)]).split(app_area) + }; + + let (sidebar, main) = if body_chunks.len() == 2 { + (Some(body_chunks[0]), body_chunks[1]) + } else { + (None, body_chunks[0]) + }; + + let header = Rect { + x: main.x, + y: main.y, + width: main.width, + height: 3.min(main.height), + }; + let composer_y = main + .y + .saturating_add(main.height.saturating_sub(COMPOSER_HEIGHT)); + let composer = Rect { + x: main.x.saturating_add(1), + y: composer_y, + width: main.width.saturating_sub(2), + height: COMPOSER_HEIGHT, + }; + let content_y = main.y.saturating_add(header.height); + let content = Rect { + x: main.x, + y: content_y, + width: main.width, + height: composer.y.saturating_sub(content_y), + }; + + Self { + sidebar, + header, + content, + composer, + footer, + } + } +} + +const THREADS: &[ThreadItem] = &[ + ThreadItem { + title: "Count Rs in strawberry", + group: "Today", + preview: "There are three Rs in strawberry.", + }, + ThreadItem { + title: "New thread", + group: "Today", + preview: "A blank chat ready for the next idea.", + }, + ThreadItem { + title: "New thread", + group: "Today", + preview: "Saved conversation placeholder.", + }, + ThreadItem { + title: "New thread", + group: "Today", + preview: "Another recent thread.", + }, +]; + +const PROMPTS: &[&str] = &[ + "How does AI work?", + "Are black holes real?", + "How many Rs are in the word \"strawberry\"?", + "What is the meaning of life?", +]; + +fn main() -> io::Result<()> { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + + let state = Rc::new(RefCell::new(DemoState::default())); + let backend = WebGl2Backend::new_with_options( + WebGl2BackendOptions::new() + .disable_auto_css_resize() + .canvas_padding_color(page_bg()), + )?; + let mut terminal = Terminal::new(backend)?; + + terminal.on_key_event({ + let state = state.clone(); + move |key| state.borrow_mut().handle_key(key) + })?; + + terminal.on_mouse_event({ + let state = state.clone(); + move |event| state.borrow_mut().handle_mouse(event) + })?; + + terminal.draw_web(move |frame| { + let mut state = state.borrow_mut(); + draw(frame, &mut state); + }); + + Ok(()) +} + +fn draw(frame: &mut Frame<'_>, state: &mut DemoState) { + let area = frame.area(); + state.viewport = area; + set_active_palette(state); + sync_overlay_state(state); + Clear.render(area, frame.buffer_mut()); + frame.render_widget(Block::new().style(Style::default().bg(page_bg())), area); + + let layout = AppLayout::from_area(area, state.sidebar_open); + if let Some(sidebar) = layout.sidebar { + draw_sidebar(frame, sidebar, state); + } + + draw_main_header(frame, layout.header, state); + if layout.sidebar.is_none() { + draw_compact_header(frame, layout.header); + } + + if state.main_view == MainView::Settings { + draw_settings_view(frame, settings_view_area(&layout), state); + } else { + draw_timeline(frame, layout.content, state); + draw_composer(frame, layout.composer, state); + } + + draw_footer(frame, layout.footer, state); + draw_popover(frame, &layout, state); +} + +fn draw_sidebar(frame: &mut Frame<'_>, area: Rect, state: &DemoState) { + let block = Block::new().style(Style::default().bg(sidebar_bg())); + frame.render_widget(block, area); + + let inner = Rect { + x: area.x + 1, + y: area.y, + width: area.width.saturating_sub(2), + height: area.height, + }; + + let [header, new_chat, search, threads, actions] = Layout::vertical([ + Constraint::Length(3), + Constraint::Length(2), + Constraint::Length(3), + Constraint::Min(8), + Constraint::Length(4), + ]) + .areas(inner); + + frame.render_widget( + Paragraph::new(Line::from(vec![ + Span::styled( + "T1 Chat", + Style::default().fg(text()).add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::styled(" ALPHA ", Style::default().fg(muted()).bg(surface_alt())), + ])) + .alignment(Alignment::Center), + Rect { + y: header.y.saturating_add(1), + height: 1, + ..header + }, + ); + + frame.render_widget( + Paragraph::new(Line::from(Span::styled( + "New Chat", + Style::default() + .fg(Color::Rgb(255, 255, 255)) + .add_modifier(Modifier::BOLD), + ))) + .alignment(Alignment::Center) + .style( + Style::default() + .fg(Color::Rgb(255, 255, 255)) + .bg(new_chat_bg()), + ), + Rect { + x: new_chat.x, + y: new_chat.y, + width: new_chat.width, + height: 1, + }, + ); + + let search_text = Line::from(vec![ + Span::raw(" "), + Span::styled("Search ", Style::default().fg(text())), + Span::styled("your threads...", Style::default().fg(muted())), + ]); + frame.render_widget( + Paragraph::new(search_text).style(Style::default().bg(sidebar_bg())), + search, + ); + + let mut rows = Vec::new(); + let mut last_group = ""; + for (index, thread) in THREADS.iter().enumerate() { + if thread.group != last_group { + rows.push(ListItem::new(Line::from(Span::styled( + thread.group, + Style::default().fg(muted()), + )))); + last_group = thread.group; + } + rows.push(thread_row( + index, + state.active_thread == Some(index), + thread, + )); + } + + frame.render_widget(List::new(rows), threads); + + let profile_icon = Rect { + x: actions.x + actions.width.saturating_sub(16), + y: actions.y.saturating_add(1), + width: 3, + height: 3.min(actions.height.saturating_sub(1)), + }; + frame.render_widget( + Block::new().style(Style::default().fg(text()).bg(control_active())), + profile_icon, + ); +} + +fn draw_compact_header(frame: &mut Frame<'_>, area: Rect) { + let [brand, _] = Layout::horizontal([Constraint::Length(20), Constraint::Min(0)]).areas(area); + frame.render_widget( + Paragraph::new(Line::from(vec![ + Span::styled( + " T1 Chat", + Style::default().fg(text()).add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::styled(" ALPHA ", Style::default().fg(muted()).bg(surface_alt())), + ])), + brand, + ); +} + +fn draw_main_header(frame: &mut Frame<'_>, area: Rect, _state: &DemoState) { + let top_band = Rect { + y: area.y.saturating_add(1), + height: area.height.saturating_sub(1), + width: area.width.saturating_sub(8), + ..area + }; + let controls = Rect { + x: area.x.saturating_add(area.width.saturating_sub(8)), + y: area.y.saturating_add(1), + width: 8.min(area.width), + height: 1, + ..area + }; + frame.render_widget( + Block::new().style(Style::default().bg(panel_bg())), + top_band, + ); + frame.render_widget( + Paragraph::new(" ") + .alignment(Alignment::Center) + .style(Style::default().bg(panel_bg())), + controls, + ); +} + +fn draw_timeline(frame: &mut Frame<'_>, area: Rect, state: &DemoState) { + let block = Block::new() + .borders(Borders::RIGHT) + .border_style(if state.focus == FocusArea::Timeline { + Style::default().fg(accent()) + } else { + Style::default().fg(divider()) + }) + .style(Style::default().bg(panel_bg())) + .padding(Padding::horizontal(3)); + frame.render_widget(block, area); + let inner = timeline_inner(area); + + if state.active_thread.is_none() && state.sent_prompt.is_none() { + draw_welcome(frame, inner, state); + return; + } + + let lines = if let Some(prompt) = &state.sent_prompt { + sent_chat_lines(prompt, state.turn_count) + } else { + let active = state.active_thread.unwrap_or(0); + thread_chat_lines(&THREADS[active]) + }; + + frame.render_widget( + Paragraph::new(Text::from(lines)) + .wrap(Wrap { trim: false }) + .style(Style::default().fg(text()).bg(panel_bg())), + inner, + ); +} + +fn draw_welcome(frame: &mut Frame<'_>, area: Rect, state: &DemoState) { + let status = if state.temporary_chat { + Line::from(Span::styled( + " Temporary chat", + Style::default().fg(accent()).bg(panel_bg()), + )) + } else { + Line::from(Span::styled( + "How can I help you?", + Style::default().fg(text()).bg(panel_bg()), + )) + }; + + frame.render_widget( + Paragraph::new(status).style(Style::default().bg(panel_bg())), + Rect { + y: area.y.saturating_add(5), + height: 1, + ..area + }, + ); + + for (index, card) in welcome_card_rects(area) { + let label = WELCOME_CARDS[index].0; + frame.render_widget( + Block::bordered() + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(divider())) + .style(Style::default().bg(surface_alt())), + card, + ); + frame.render_widget( + Paragraph::new(label) + .alignment(Alignment::Center) + .style(Style::default().fg(text()).bg(surface_alt())), + Rect { + x: card.x.saturating_add(1), + y: card.y.saturating_add(1), + width: card.width.saturating_sub(2), + height: 1, + }, + ); + } + + let prompt_y = area.y.saturating_add(welcome_prompt_start(area.width)); + for (index, prompt) in PROMPTS.iter().take(MAX_VISIBLE_PROMPTS).enumerate() { + let y = prompt_y.saturating_add(index as u16 * 2); + frame.render_widget( + Paragraph::new(truncate(prompt, area.width.saturating_sub(1) as usize)) + .style(Style::default().fg(muted()).bg(panel_bg())), + Rect { + y, + height: 1, + ..area + }, + ); + frame.render_widget( + Paragraph::new("─".repeat(area.width as usize)) + .style(Style::default().fg(divider()).bg(panel_bg())), + Rect { + y: y.saturating_add(1), + height: 1, + ..area + }, + ); + } +} + +fn draw_settings_view(frame: &mut Frame<'_>, area: Rect, state: &DemoState) { + let block = Block::new() + .borders(Borders::RIGHT) + .border_style(Style::default().fg(divider())) + .style(Style::default().bg(panel_bg())) + .padding(Padding::horizontal(3)); + let inner = block.inner(area); + frame.render_widget(block, area); + + let lines = vec![ + Line::from(""), + Line::from(Span::styled( + "Settings", + Style::default() + .fg(text()) + .bg(panel_bg()) + .add_modifier(Modifier::BOLD), + )), + Line::from(""), + settings_section("General"), + settings_row("Mode", "T1 Chat", false), + settings_row("Theme", state.theme_label(), true), + settings_row( + "Theme preset", + if state.boring_mode { + "Boring" + } else { + "Default" + }, + true, + ), + settings_row("Assistant responses", "Streaming", false), + settings_row("Thread workspace", "Local", false), + settings_row("Confirm thread delete", "Enabled", false), + settings_row("Temporary chat", bool_label(state.temporary_chat), true), + Line::from(""), + settings_section("Models"), + settings_row("Text generation model", state.model_label(), true), + Line::from(""), + settings_section("Advanced"), + settings_row("Keybindings", "Open file", false), + settings_row("Back to thread", "Return", true), + ]; + + frame.render_widget( + Paragraph::new(Text::from(lines)) + .wrap(Wrap { trim: false }) + .style(Style::default().fg(text()).bg(panel_bg())), + inner, + ); +} + +fn settings_view_area(layout: &AppLayout) -> Rect { + Rect { + x: layout.content.x, + y: layout.content.y, + width: layout.content.width, + height: layout + .composer + .y + .saturating_add(layout.composer.height) + .saturating_sub(layout.content.y), + } +} + +fn settings_section(label: &'static str) -> Line<'static> { + Line::from(Span::styled( + label, + Style::default() + .fg(muted()) + .bg(panel_bg()) + .add_modifier(Modifier::BOLD), + )) +} + +fn settings_row(label: &'static str, value: &'static str, actionable: bool) -> Line<'static> { + let marker = if actionable { "> " } else { " " }; + Line::from(vec![ + Span::styled(marker, Style::default().fg(accent()).bg(panel_bg())), + Span::styled( + format!("{label:<24}"), + Style::default() + .fg(if actionable { text() } else { muted() }) + .bg(panel_bg()), + ), + Span::styled( + value.to_string(), + Style::default().fg(text()).bg(panel_bg()), + ), + ]) +} + +fn timeline_inner(area: Rect) -> Rect { + Rect { + x: area.x.saturating_add(3), + y: area.y, + width: area.width.saturating_sub(7), + height: area.height, + } +} + +fn welcome_card_rects(area: Rect) -> Vec<(usize, Rect)> { + let mut rects = Vec::new(); + let mut x = area.x; + let mut y = area.y.saturating_add(WELCOME_CARD_ROW); + let right = area.x.saturating_add(area.width); + + for (index, &(_, width)) in WELCOME_CARDS.iter().enumerate() { + if x > area.x && x.saturating_add(width) > right { + x = area.x; + y = y.saturating_add(3); + } + rects.push(( + index, + Rect { + x, + y, + width, + height: 3, + }, + )); + x = x.saturating_add(width).saturating_add(WELCOME_CARD_GAP); + } + + rects +} + +fn welcome_prompt_start(width: u16) -> u16 { + WELCOME_CARD_ROW + .saturating_add(welcome_card_rows(width).saturating_mul(3)) + .saturating_add(2) +} + +fn welcome_card_rows(width: u16) -> u16 { + let mut rows: u16 = 1; + let mut used: u16 = 0; + for &(_, card_width) in WELCOME_CARDS { + let next_width = if used == 0 { + card_width + } else { + used.saturating_add(WELCOME_CARD_GAP) + .saturating_add(card_width) + }; + if used > 0 && next_width > width { + rows = rows.saturating_add(1); + used = card_width; + } else { + used = next_width; + } + } + rows +} + +fn draw_composer(frame: &mut Frame<'_>, area: Rect, state: &DemoState) { + let border_style = if state.focus == FocusArea::Composer { + Style::default().fg(composer_border()) + } else { + Style::default().fg(divider()) + }; + let block = Block::new() + .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT) + .border_type(BorderType::Rounded) + .border_style(border_style) + .style(Style::default().bg(surface())); + frame.render_widget(block, area); + + let input_area = Rect { + x: area.x.saturating_add(2), + y: area.y.saturating_add(2), + width: area.width.saturating_sub(4), + height: area.height.saturating_sub(4), + }; + let toolbar_area = Rect { + x: area.x.saturating_add(3), + y: area.y.saturating_add(area.height.saturating_sub(2)), + width: area.width.saturating_sub(6), + height: 1, + }; + + let composer_text = if state.composer.is_empty() { + Text::from(Line::from(Span::styled( + "Type your message here...", + Style::default().fg(muted()), + ))) + } else { + Text::from(Line::from(Span::styled( + state.composer.as_str(), + Style::default().fg(text()), + ))) + }; + + frame.render_widget( + Paragraph::new(composer_text) + .wrap(Wrap { trim: false }) + .style(Style::default().fg(text()).bg(surface())), + input_area, + ); + + let toolbar_clear_style = Style::default().fg(muted()).bg(surface()); + let [model, separator, mode, _, _send_slot] = Layout::horizontal([ + Constraint::Length(14), + Constraint::Length(3), + Constraint::Length(10), + Constraint::Min(0), + Constraint::Length(5), + ]) + .areas(toolbar_area); + frame.render_widget( + Paragraph::new(Line::from(Span::styled(" ", toolbar_clear_style))) + .style(toolbar_clear_style), + model, + ); + frame.render_widget( + Paragraph::new(Line::from(Span::styled( + " ", + Style::default().bg(surface()), + ))) + .style(Style::default().bg(surface())), + separator, + ); + frame.render_widget( + Paragraph::new(Line::from(Span::styled(" ", toolbar_clear_style))) + .style(toolbar_clear_style), + mode, + ); + let send_bg = if state.composer.trim().is_empty() { + surface_alt() + } else { + send_active() + }; + let send = Rect { + x: area.x.saturating_add(area.width.saturating_sub(8)), + y: area.y.saturating_add(area.height.saturating_sub(3)), + width: 5.min(area.width.saturating_sub(2)), + height: 2.min(area.height.saturating_sub(2)), + }; + frame.render_widget( + Block::new().style(Style::default().fg(text()).bg(send_bg)), + send, + ); +} + +fn draw_footer(frame: &mut Frame<'_>, area: Rect, _state: &DemoState) { + if area.height == 0 { + return; + } + + frame.render_widget(Block::new().style(Style::default().bg(page_bg())), area); +} + +fn draw_popover(frame: &mut Frame<'_>, layout: &AppLayout, state: &DemoState) { + let Some(area) = popover_rect(layout, state.popover) else { + return; + }; + + Clear.render(area, frame.buffer_mut()); + if matches!(state.popover, Popover::Model | Popover::Thinking) { + frame.render_widget(Block::new().style(Style::default().bg(surface_alt())), area); + let inner = Rect { + x: area.x.saturating_add(2), + y: area.y.saturating_add(1), + width: area.width.saturating_sub(4), + height: area.height.saturating_sub(2), + }; + let lines = match state.popover { + Popover::Model => model_menu_lines(state), + Popover::Thinking => thinking_menu_lines(state), + _ => Vec::new(), + }; + frame.render_widget( + Paragraph::new(Text::from(lines)) + .wrap(Wrap { trim: false }) + .style(Style::default().bg(surface_alt())), + inner, + ); + if state.popover == Popover::Model { + frame.render_widget( + Paragraph::new(Text::from(vec![ + Line::from(Span::styled("│", Style::default().fg(subtle()))), + Line::from(Span::styled("│", Style::default().fg(subtle()))), + Line::from(Span::styled("│", Style::default().fg(subtle()))), + Line::from(Span::styled("│", Style::default().fg(subtle()))), + Line::from(Span::styled("│", Style::default().fg(subtle()))), + ])) + .style(Style::default().bg(surface_alt())), + Rect { + x: area.x.saturating_add(area.width.saturating_sub(4)), + y: area.y.saturating_add(1), + width: 1, + height: area.height.saturating_sub(2), + }, + ); + } + return; + } + + let title = match state.popover { + Popover::Model => " model ", + Popover::Thinking => " effort ", + Popover::Settings => " chat ", + Popover::SidebarSettings => " profiles ", + Popover::QuickAction(index) => quick_action_title(index), + Popover::None => "", + }; + + let block = Block::bordered() + .title(title) + .border_style(Style::default().fg(accent())) + .style(Style::default().bg(surface_alt())); + let inner = block.inner(area); + frame.render_widget(block, area); + + let lines = match state.popover { + Popover::Model => model_menu_lines(state), + Popover::Thinking => thinking_menu_lines(state), + Popover::Settings => chat_settings_lines(state), + Popover::SidebarSettings => profile_settings_lines(state), + Popover::QuickAction(index) => vec![ + Line::from(Span::styled( + "Choose this mode", + Style::default().fg(subtle()).bg(surface_alt()), + )), + option_line(quick_action_prompt(index), false), + ], + Popover::None => Vec::new(), + }; + + frame.render_widget( + Paragraph::new(Text::from(lines)) + .wrap(Wrap { trim: false }) + .style(Style::default().bg(surface_alt())), + inner, + ); +} + +fn popover_rect(layout: &AppLayout, popover: Popover) -> Option { + match popover { + Popover::None => None, + Popover::Model => Some(Rect { + x: layout.composer.x.saturating_add(3), + y: layout.composer.y.saturating_sub(8), + width: 40.min(layout.composer.width.saturating_sub(6)), + height: 8, + }), + Popover::Thinking => Some(Rect { + x: layout.composer.x.saturating_add(24), + y: layout.composer.y.saturating_sub(11), + width: 44.min(layout.composer.width.saturating_sub(28)), + height: 11, + }), + Popover::Settings => Some(Rect { + x: layout + .header + .x + .saturating_add(layout.header.width.saturating_sub(31)), + y: layout.header.y.saturating_add(3), + width: 30, + height: 6, + }), + Popover::SidebarSettings => layout.sidebar.map(|sidebar| Rect { + x: sidebar.x.saturating_add(3), + y: sidebar.y.saturating_add(sidebar.height.saturating_sub(12)), + width: sidebar.width.saturating_sub(5), + height: 8, + }), + Popover::QuickAction(index) => { + let content = timeline_inner(layout.content); + welcome_card_rects(content) + .into_iter() + .find(|(card_index, _)| *card_index == index) + .map(|(_, card)| Rect { + x: card.x, + y: card.y.saturating_add(3), + width: 24.min(content.width), + height: 5, + }) + } + } +} + +fn chat_settings_lines(_state: &DemoState) -> Vec> { + vec![ + blank_popover_line(), + blank_popover_line(), + blank_popover_line(), + blank_popover_line(), + ] +} + +fn profile_settings_lines(state: &DemoState) -> Vec> { + if state.profile_create_open { + return vec![ + Line::from(Span::styled( + "Create a Profile", + Style::default() + .fg(text()) + .bg(surface_alt()) + .add_modifier(Modifier::BOLD), + )), + Line::from(Span::styled( + "Separate threads", + Style::default().fg(subtle()).bg(surface_alt()), + )), + Line::from(vec![ + Span::styled(" Icon ", Style::default().fg(muted()).bg(surface_alt())), + Span::styled("Default", Style::default().fg(text()).bg(surface_alt())), + ]), + Line::from(Span::styled( + "Name Default", + Style::default().fg(muted()).bg(surface_alt()), + )), + option_line("Create Profile", false), + ]; + } + + vec![ + Line::from(Span::styled( + "Default profile", + Style::default() + .fg(text()) + .bg(surface_alt()) + .add_modifier(Modifier::BOLD), + )), + option_line("T1 Chat", true), + option_line("Create profile", false), + ] +} + +fn model_menu_lines(state: &DemoState) -> Vec> { + vec![ + menu_line(" Codex", state.model_choice == ModelChoice::Gpt54, false), + menu_line(" Claude", state.model_choice == ModelChoice::Claude, false), + menu_line(" Cursor Soon", false, true), + menu_line(" OpenCode Soon", false, true), + menu_line(" Gemini Soon", false, true), + ] +} + +fn thinking_menu_lines(state: &DemoState) -> Vec> { + vec![ + section_line("Reasoning"), + menu_line(" Extra High", false, false), + menu_line(" High (default)", false, false), + menu_line(" Medium", false, false), + menu_line(" Low", state.thinking_mode == ThinkingMode::Think, false), + blank_popover_line(), + section_line("Fast Mode"), + menu_line(" Off", state.thinking_mode == ThinkingMode::Think, false), + menu_line(" On", state.thinking_mode == ThinkingMode::Fast, false), + ] +} + +fn section_line(label: &'static str) -> Line<'static> { + Line::from(Span::styled( + label, + Style::default().fg(muted()).bg(surface_alt()), + )) +} + +fn menu_line(label: &str, active: bool, disabled: bool) -> Line<'static> { + let padded_label = format!("{label:<34}"); + let style = Style::default() + .fg(if disabled { + subtle() + } else if active { + text() + } else { + muted() + }) + .bg(if active { + control_active() + } else { + surface_alt() + }) + .add_modifier(if active { + Modifier::BOLD + } else { + Modifier::empty() + }); + Line::from(Span::styled(padded_label, style)) +} + +fn blank_popover_line() -> Line<'static> { + Line::from(Span::styled( + " ".repeat(26), + Style::default().bg(surface_alt()), + )) +} + +fn option_line(label: &str, active: bool) -> Line<'static> { + let prefix = if active { "> " } else { " " }; + Line::from(vec![ + Span::styled(prefix, Style::default().fg(accent()).bg(surface_alt())), + Span::styled( + label.to_string(), + Style::default().fg(text()).bg(surface_alt()), + ), + ]) +} + +fn bool_label(enabled: bool) -> &'static str { + if enabled { + "On" + } else { + "Off" + } +} + +fn quick_action_title(index: usize) -> &'static str { + match index { + 0 => " create ", + 1 => " explore ", + 2 => " code ", + 3 => " learn ", + _ => " action ", + } +} + +fn quick_action_prompt(index: usize) -> &'static str { + match index { + 0 => "Create a concise project plan", + 1 => "Explore an idea from first principles", + 2 => "Write a small code example", + 3 => "Teach me the key concept", + _ => "Start a new thread with a prompt", + } +} + +fn sync_overlay_state(state: &DemoState) { + let view_class = if state.main_view == MainView::Settings { + "is-settings" + } else if state.main_view == MainView::Thread + && state.active_thread.is_none() + && state.sent_prompt.is_none() + { + "is-welcome" + } else { + "is-chat" + }; + + let theme_class = match state.theme_choice { + ThemeChoice::Light => "theme-light", + ThemeChoice::System => "theme-system", + ThemeChoice::Dark => "theme-dark", + }; + let boring_class = if state.boring_mode { + "boring-on" + } else { + "boring-off" + }; + let popover_class = match state.popover { + Popover::Model => "popover-model", + Popover::Thinking => "popover-thinking", + Popover::Settings => "popover-chat-settings", + Popover::SidebarSettings => "popover-profiles", + _ => "popover-none", + }; + let profile_class = if state.profile_create_open { + "profile-create-open" + } else { + "profile-create-closed" + }; + let composer_class = if state.composer.trim().is_empty() { + "composer-empty" + } else { + "composer-filled" + }; + let temporary_class = if state.temporary_chat { + "temporary-on" + } else { + "temporary-off" + }; + let thinking_class = match state.thinking_mode { + ThinkingMode::Fast => "thinking-fast", + ThinkingMode::Think => "thinking-low", + }; + let model_class = match state.model_choice { + ModelChoice::Gpt54 => "model-gpt54", + ModelChoice::Gpt5Mini => "model-gpt5m", + ModelChoice::Claude => "model-claude", + }; + let class_name = format!( + "{view_class} {theme_class} {boring_class} {popover_class} {profile_class} {composer_class} {temporary_class} {thinking_class} {model_class}" + ); + + if let Some(window) = ratzilla::web_sys::window() { + if let Some(document) = window.document() { + if let Some(body) = document.body() { + body.set_class_name(&class_name); + } + } + } +} + +fn thread_row(_index: usize, active: bool, thread: &ThreadItem) -> ListItem<'static> { + let style = if active { + Style::default() + .fg(text()) + .bg(surface_alt()) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(muted()) + }; + ListItem::new(Line::from(Span::styled(truncate(thread.title, 25), style))) +} + +fn thread_chat_lines(thread: &ThreadItem) -> Vec> { + vec![ + role_line("user", " 10:42 AM", accent()), + Line::from(match thread.title { + "Count Rs in strawberry" => "How many Rs are in the word \"strawberry\"?", + "New thread" => "Start a new thread with a prompt.", + _ => "Can you help me think this through?", + }), + Line::from(""), + role_line("assistant", " 10:43 AM", accent()), + Line::from(thread.preview), + Line::from(""), + Line::from("The short answer is visible in the conversation, with the context"), + Line::from("kept nearby so you can continue from the same thread later."), + Line::from(""), + Line::from(Span::styled( + "Ready for the next prompt.", + Style::default().fg(muted()), + )), + ] +} + +fn sent_chat_lines(prompt: &str, turn_count: usize) -> Vec> { + vec![ + role_line("user", " just now", accent()), + Line::from(prompt.to_string()), + Line::from(""), + role_line("assistant", " streaming", accent()), + Line::from(format!("Demo turn {turn_count}: I can work with that.")), + Line::from(""), + Line::from("Here is a concise answer in the same conversation surface,"), + Line::from("with the prompt preserved above and the composer ready for"), + Line::from("the next turn."), + ] +} + +fn role_line<'a>(role: &'a str, time: &'a str, color: Color) -> Line<'a> { + Line::from(vec![ + Span::styled( + role, + Style::default().fg(color).add_modifier(Modifier::BOLD), + ), + Span::styled(time, Style::default().fg(subtle())), + ]) +} + +fn thread_index_for_sidebar_row(row: u16) -> Option { + match row { + 9 => Some(0), + 10 => Some(1), + 11 => Some(2), + 12 => Some(3), + _ => None, + } +} + +fn contains(rect: Rect, x: u16, y: u16) -> bool { + x >= rect.x + && x < rect.x.saturating_add(rect.width) + && y >= rect.y + && y < rect.y.saturating_add(rect.height) +} + +fn set_active_palette(state: &DemoState) { + ACTIVE_PALETTE.with(|palette| palette.set(state.palette())); +} + +fn palette_color(select: impl FnOnce(DemoPalette) -> Color) -> Color { + ACTIVE_PALETTE.with(|palette| select(palette.get())) +} + +fn accent() -> Color { + palette_color(|palette| palette.accent) +} + +fn new_chat_bg() -> Color { + palette_color(|palette| palette.new_chat_bg) +} + +fn composer_border() -> Color { + palette_color(|palette| palette.composer_border) +} + +fn send_active() -> Color { + palette_color(|palette| palette.send_active) +} + +fn control_active() -> Color { + palette_color(|palette| palette.control_active) +} + +fn text() -> Color { + palette_color(|palette| palette.text) +} + +fn muted() -> Color { + palette_color(|palette| palette.muted) +} + +fn subtle() -> Color { + palette_color(|palette| palette.subtle) +} + +fn divider() -> Color { + palette_color(|palette| palette.divider) +} + +fn page_bg() -> Color { + palette_color(|palette| palette.page_bg) +} + +fn sidebar_bg() -> Color { + palette_color(|palette| palette.sidebar_bg) +} + +fn panel_bg() -> Color { + palette_color(|palette| palette.panel_bg) +} + +fn surface() -> Color { + palette_color(|palette| palette.surface) +} + +fn surface_alt() -> Color { + palette_color(|palette| palette.surface_alt) +} + +fn truncate(input: &str, max_width: usize) -> String { + if input.chars().count() <= max_width { + return input.to_string(); + } + + if max_width <= 1 { + return ".".to_string(); + } + + let mut value = input.chars().take(max_width - 1).collect::(); + value.push('.'); + value +} diff --git a/apps/tui/package.json b/apps/tui/package.json index c7ac48a9..ee64c027 100644 --- a/apps/tui/package.json +++ b/apps/tui/package.json @@ -14,8 +14,8 @@ }, "bin": { "t1": "./bin/t1code.js", - "t1code": "./bin/t1code.js", - "t1chat": "./bin/t1chat.js" + "t1chat": "./bin/t1chat.js", + "t1code": "./bin/t1code.js" }, "files": [ "README.md", diff --git a/apps/tui/src/ui.tsx b/apps/tui/src/ui.tsx index 619334f7..a6f4d4e0 100644 --- a/apps/tui/src/ui.tsx +++ b/apps/tui/src/ui.tsx @@ -1318,11 +1318,7 @@ function AttachmentPill({ ); } -function ChatCategoryButton(props: { - icon: string; - label: string; - onPress: () => void; -}) { +function ChatCategoryButton(props: { icon: string; label: string; onPress: () => void }) { const [hoveredCategory, setHoveredCategory] = useState(false); return ( - + - + {responsiveLayout.showWindowDots && !isChatMode ? : null} - + {sidebarSearchQuery ? null : ( - {isChatMode ? null : { - toggleSidebarSortMenu(event); + {isChatMode ? null : ( + { + toggleSidebarSortMenu(event); + }, }, - }, - { - icon: "+", - onPress: () => { - openProjectPathPrompt(); + { + icon: "+", + onPress: () => { + openProjectPathPrompt(); + }, }, - }, - ]} - />} + ]} + /> + )} {!isChatMode && projects.length === 0 ? ( ) : null} - {isChatMode ? (() => { - const allThreads = sortedProjects.flatMap((project) => { - const projectThreads = threadsByProject.get(project.id) ?? []; - return projectThreads.map((thread) => ({ ...thread, projectId: project.id })); - }); - allThreads.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); - const profileThreads = allThreads.filter((t) => { - const threadProfile = threadProfileMap[t.id]; - if (!threadProfile) return activeProfileId === "default"; - return threadProfile === activeProfileId; - }); - const searchLower = sidebarSearchQuery.toLowerCase().trim(); - const filteredThreads = searchLower - ? profileThreads.filter((t) => t.title.toLowerCase().includes(searchLower)) - : profileThreads; - - const now = new Date(); - const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const yesterdayStart = new Date(todayStart.getTime() - 86400000); - const weekStart = new Date(todayStart.getTime() - 7 * 86400000); - const monthStart = new Date(todayStart.getTime() - 30 * 86400000); - - type TimeGroup = { label: string; threads: typeof allThreads }; - const groups: TimeGroup[] = [ - { label: "Today", threads: [] }, - { label: "Yesterday", threads: [] }, - { label: "Last 7 Days", threads: [] }, - { label: "Last 30 Days", threads: [] }, - { label: "Older", threads: [] }, - ]; - - for (const thread of filteredThreads) { - const date = new Date(thread.updatedAt); - if (date >= todayStart) groups[0]!.threads.push(thread); - else if (date >= yesterdayStart) groups[1]!.threads.push(thread); - else if (date >= weekStart) groups[2]!.threads.push(thread); - else if (date >= monthStart) groups[3]!.threads.push(thread); - else groups[4]!.threads.push(thread); - } - - return groups.filter((g) => g.threads.length > 0).map((group) => ( - - - {group.threads.map((thread) => { - const isActive = thread.id === activeThreadId; - const isSelected = selectedThreadIds.has(thread.id); - const status = threadStatus(thread, { - forceUnread: locallyUnreadThreadIds.has(thread.id), - locallyVisitedAt: locallyVisitedThreads[thread.id], - }); - return ( - { - closeSidebarContextMenu(); - handleThreadClick( - event, - thread.projectId, - thread.id, - allThreads.map((t) => t.id), - ); - }} - onSecondaryPress={(event) => { - openThreadContextMenu(thread.projectId, thread.id, event); - }} - > - - {status ? ( - - ) : null} - - - - - - ); - })} - - )); - })() : null} - - {!isChatMode ? sortedProjects.map((project) => { - const projectThreads = threadsByProject.get(project.id) ?? []; - const orderedProjectThreadIds = projectThreads.map((thread) => thread.id); - const isProjectExpanded = expandedProjectIds.has(project.id); - const isProjectActive = project.id === activeProjectId; - const projectStatus = resolveProjectStatusIndicator( - projectThreads.map((thread) => - resolveThreadStatusPillForTui(thread, { - forceUnread: locallyUnreadThreadIds.has(thread.id), - locallyVisitedAt: locallyVisitedThreads[thread.id], - }), - ), - ); - - return ( - - { - closeSidebarContextMenu(); - if (selectedThreadIds.size > 0) { - clearSelection(); - } - selectProject(project.id); - setExpandedProjectIds((current) => - resolveProjectExpansionOnRowPress({ - expandedProjectIds: current, - projectId: project.id, - isProjectActive, - }), - ); - }} - onSecondaryPress={(event) => { - openProjectContextMenu(project.id, event); - }} - > - - - - { - closeSidebarContextMenu(); - if (selectedThreadIds.size > 0) { - clearSelection(); - } - selectProject(project.id); - openDraftThread(project.id); - }} - /> - + {isChatMode + ? (() => { + const allThreads = sortedProjects.flatMap((project) => { + const projectThreads = threadsByProject.get(project.id) ?? []; + return projectThreads.map((thread) => ({ ...thread, projectId: project.id })); + }); + allThreads.sort( + (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), + ); + const profileThreads = allThreads.filter((t) => { + const threadProfile = threadProfileMap[t.id]; + if (!threadProfile) return activeProfileId === "default"; + return threadProfile === activeProfileId; + }); + const searchLower = sidebarSearchQuery.toLowerCase().trim(); + const filteredThreads = searchLower + ? profileThreads.filter((t) => t.title.toLowerCase().includes(searchLower)) + : profileThreads; + + const now = new Date(); + const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const yesterdayStart = new Date(todayStart.getTime() - 86400000); + const weekStart = new Date(todayStart.getTime() - 7 * 86400000); + const monthStart = new Date(todayStart.getTime() - 30 * 86400000); + + type TimeGroup = { label: string; threads: typeof allThreads }; + const groups: TimeGroup[] = [ + { label: "Today", threads: [] }, + { label: "Yesterday", threads: [] }, + { label: "Last 7 Days", threads: [] }, + { label: "Last 30 Days", threads: [] }, + { label: "Older", threads: [] }, + ]; + + for (const thread of filteredThreads) { + const date = new Date(thread.updatedAt); + if (date >= todayStart) groups[0]!.threads.push(thread); + else if (date >= yesterdayStart) groups[1]!.threads.push(thread); + else if (date >= weekStart) groups[2]!.threads.push(thread); + else if (date >= monthStart) groups[3]!.threads.push(thread); + else groups[4]!.threads.push(thread); + } - {isProjectExpanded ? ( - - {projectThreads.length > 0 ? ( - projectThreads.map((thread) => { + return groups + .filter((g) => g.threads.length > 0) + .map((group) => ( + + + {group.threads.map((thread) => { + const isActive = thread.id === activeThreadId; + const isSelected = selectedThreadIds.has(thread.id); const status = threadStatus(thread, { forceUnread: locallyUnreadThreadIds.has(thread.id), locallyVisitedAt: locallyVisitedThreads[thread.id], }); - const isActive = thread.id === activeThreadId; - const isSelected = selectedThreadIds.has(thread.id); return ( t.id), ); }} onSecondaryPress={(event) => { - openThreadContextMenu(project.id, thread.id, event); + openThreadContextMenu(thread.projectId, thread.id, event); }} > - - - ); - }) - ) : ( + })} + + )); + })() + : null} + + {!isChatMode + ? sortedProjects.map((project) => { + const projectThreads = threadsByProject.get(project.id) ?? []; + const orderedProjectThreadIds = projectThreads.map((thread) => thread.id); + const isProjectExpanded = expandedProjectIds.has(project.id); + const isProjectActive = project.id === activeProjectId; + const projectStatus = resolveProjectStatusIndicator( + projectThreads.map((thread) => + resolveThreadStatusPillForTui(thread, { + forceUnread: locallyUnreadThreadIds.has(thread.id), + locallyVisitedAt: locallyVisitedThreads[thread.id], + }), + ), + ); + + return ( + + { + closeSidebarContextMenu(); + if (selectedThreadIds.size > 0) { + clearSelection(); + } + selectProject(project.id); + setExpandedProjectIds((current) => + resolveProjectExpansionOnRowPress({ + expandedProjectIds: current, + projectId: project.id, + isProjectActive, + }), + ); + }} + onSecondaryPress={(event) => { + openProjectContextMenu(project.id, event); + }} + > + + + + { + closeSidebarContextMenu(); + if (selectedThreadIds.size > 0) { + clearSelection(); + } + selectProject(project.id); + openDraftThread(project.id); + }} + /> + + + {isProjectExpanded ? ( - + {projectThreads.length > 0 ? ( + projectThreads.map((thread) => { + const status = threadStatus(thread, { + forceUnread: locallyUnreadThreadIds.has(thread.id), + locallyVisitedAt: locallyVisitedThreads[thread.id], + }); + const isActive = thread.id === activeThreadId; + const isSelected = selectedThreadIds.has(thread.id); + return ( + { + closeSidebarContextMenu(); + handleThreadClick( + event, + project.id, + thread.id, + orderedProjectThreadIds, + ); + }} + onSecondaryPress={(event) => { + openThreadContextMenu(project.id, thread.id, event); + }} + > + + {status ? ( + + ) : null} + + + + + + + + + ); + }) + ) : ( + + + + )} - )} + ) : null} - ) : null} - - ); - }) : null} + ); + }) + : null} {isChatMode ? ( @@ -8258,11 +8286,16 @@ export function App({ }} > - + { - setNewProfileIconIndex((i) => (i > 0 ? i - 1 : PROFILE_ICON_OPTIONS.length - 1)); + setNewProfileIconIndex((i) => + i > 0 ? i - 1 : PROFILE_ICON_OPTIONS.length - 1, + ); setProfileIconFocused(true); setProfileNameFocused(false); }} @@ -8284,8 +8317,19 @@ export function App({ /> { setProfileNameFocused(true); setProfileIconFocused(false); }} - style={{ flexGrow: 1, border: true, borderStyle: "rounded", borderColor: profileNameFocused ? PALETTE.composerBorder : PALETTE.border, backgroundColor: PALETTE.surfaceAlt, justifyContent: "center", paddingLeft: 1 }} + onMouseDown={() => { + setProfileNameFocused(true); + setProfileIconFocused(false); + }} + style={{ + flexGrow: 1, + border: true, + borderStyle: "rounded", + borderColor: profileNameFocused ? PALETTE.composerBorder : PALETTE.border, + backgroundColor: PALETTE.surfaceAlt, + justifyContent: "center", + paddingLeft: 1, + }} > - + ) : null} - + {chatProfiles.map((profile) => ( - + ) : ( - - { - if (mainView === "settings") { - returnToThreadView(); - return; - } - openMainView("settings"); + - { + if (mainView === "settings") { + returnToThreadView(); + return; + } + openMainView("settings"); }} - /> - - - { - if (mainView === "keybindings") { - returnToThreadView(); - return; - } - openMainView("keybindings"); - }} - > - + + + + { + if (mainView === "keybindings") { + returnToThreadView(); + return; + } + openMainView("keybindings"); }} - /> - - - + > + + + + )} ) : null} @@ -8450,7 +8503,15 @@ export function App({ {/* Row 2: icons row, main bg on left, sidebar bg with icons on right */} - + { - setOverlayMenu((current) => current === "chat-settings" ? null : "chat-settings"); + setOverlayMenu((current) => + current === "chat-settings" ? null : "chat-settings", + ); }} /> @@ -8483,124 +8546,126 @@ export function App({ ) : null} {isChatMode && responsiveLayout.showSidebar ? null : ( - - {isChatMode && responsiveLayout.showSidebar ? null : ( - {responsiveLayout.showSidebarToggle ? ( - toggleSidebarVisibility()} - /> - ) : null} - - - - {mainView === "thread" && activeProject && responsiveLayout.showHeaderProjectBadge ? ( - - ) : null} - - )} + {isChatMode && responsiveLayout.showSidebar ? null : ( + + {responsiveLayout.showSidebarToggle ? ( + toggleSidebarVisibility()} + /> + ) : null} + + + + {mainView === "thread" && + activeProject && + responsiveLayout.showHeaderProjectBadge ? ( + + ) : null} + + )} - {isChatMode && responsiveLayout.showSidebar ? null : ( - - {mainView === "settings" ? ( - 0} - disabled={changedSettingLabels.length === 0} - compact - marginRight={1} - onPress={() => restoreDefaultSettings()} - /> - ) : mainView === "keybindings" ? null : isChatMode ? ( - <> - setTempChatMode((prev) => !prev)} - /> - { - openMainView("settings"); - }} - /> - - ) : ( - <> - - - + {isChatMode && responsiveLayout.showSidebar ? null : ( + + {mainView === "settings" ? ( + 0} + disabled={changedSettingLabels.length === 0} + compact + marginRight={1} + onPress={() => restoreDefaultSettings()} + /> + ) : mainView === "keybindings" ? null : isChatMode ? ( + <> + setTempChatMode((prev) => !prev)} + /> + { + openMainView("settings"); + }} + /> + + ) : ( + <> + + + + )} + )} - )} - )} @@ -9412,15 +9477,28 @@ export function App({ {tempChatMode ? ( - - + + ) : ( )} - + {[ { icon: "󰛕", label: "Create" }, { icon: "󰎕", label: "Explore" }, @@ -9440,7 +9518,9 @@ export function App({ ))} - + {[ "How does AI work?", "Are black holes real?", @@ -10306,30 +10386,38 @@ export function App({ /> ) : null} - {!isChatMode && responsiveLayout.showComposerDividers ? : null} - {!isChatMode ? : null} - {!isChatMode && responsiveLayout.showComposerDividers ? : null} - {!isChatMode ? : null} + {!isChatMode && responsiveLayout.showComposerDividers ? ( + + ) : null} + {!isChatMode ? ( + + ) : null} + {!isChatMode && responsiveLayout.showComposerDividers ? ( + + ) : null} + {!isChatMode ? ( + + ) : null} {activePendingProgress ? ( <> @@ -11074,10 +11162,14 @@ export function App({ style={{ paddingLeft: 1, paddingRight: 1, - backgroundColor: appSettings.theme === "light" ? PALETTE.controlActive : "transparent", + backgroundColor: + appSettings.theme === "light" ? PALETTE.controlActive : "transparent", }} > - + { @@ -11087,10 +11179,21 @@ export function App({ style={{ paddingLeft: 1, paddingRight: 1, - backgroundColor: appSettings.theme === "system" || !appSettings.theme ? PALETTE.controlActive : "transparent", + backgroundColor: + appSettings.theme === "system" || !appSettings.theme + ? PALETTE.controlActive + : "transparent", }} > - + { @@ -11100,15 +11203,21 @@ export function App({ style={{ paddingLeft: 1, paddingRight: 1, - backgroundColor: appSettings.theme === "dark" ? PALETTE.controlActive : "transparent", + backgroundColor: + appSettings.theme === "dark" ? PALETTE.controlActive : "transparent", }} > - + { - setTuiThemeId((current) => current === BORING_THEME_ID ? "default" : BORING_THEME_ID); + setTuiThemeId((current) => + current === BORING_THEME_ID ? "default" : BORING_THEME_ID, + ); }} style={{ height: 1, flexDirection: "row", alignItems: "center" }} > From 5e0973a5c632f63a8c2bf3899a4f1fdebc009841 Mon Sep 17 00:00:00 2001 From: Ahmad Jalil Date: Sun, 5 Jul 2026 18:16:08 -0700 Subject: [PATCH 12/12] ci: enable GitHub Pages deployment --- .github/workflows/pages.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index e6470668..382eb355 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -45,6 +45,8 @@ jobs: - name: Configure Pages uses: actions/configure-pages@v5 + with: + enablement: true - name: Upload Pages artifact uses: actions/upload-pages-artifact@v4