diff --git a/README.md b/README.md index 1bb6c21c..d3559803 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,51 @@ 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 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 + +- 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/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/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/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..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"; @@ -55,44 +57,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 +107,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 +159,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", }; @@ -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 bc516814..619334f7 100644 --- a/apps/tui/src/ui.tsx +++ b/apps/tui/src/ui.tsx @@ -194,6 +194,8 @@ import { shouldClearPendingCreatedThread, } from "./threadSelection"; import { resolveWorkEntryIcon } from "./workEntryIcons"; +import { createProfile, PROFILE_ICON_OPTIONS } from "./profiles"; +import { BORING_THEME_ID } from "./theme"; type FocusArea = | "projects" @@ -213,7 +215,8 @@ type OverlayMenu = | "sidebar-sort" | "git-actions" | "composer-env" - | "composer-branch"; + | "composer-branch" + | "chat-settings"; type SettingsSelectKind = | "theme" | "theme-preset" @@ -1315,6 +1318,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; @@ -2800,6 +2838,20 @@ export function App({ const [showScrollToBottom, setShowScrollToBottom] = useState(false); const previewAttachmentCacheRef = useRef>(new Map()); const composerDraftsByThreadIdRef = useRef>>({}); + + const [isChatMode, setIsChatMode] = useState(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 })); }, []); @@ -3370,6 +3422,7 @@ export function App({ const responsiveLayout = resolveTuiResponsiveLayout({ viewportColumns: totalColumns, sidebarCollapsedPreference, + isChatMode, }); const showSidebarOverlay = !responsiveLayout.showSidebar && sidebarOverlayOpen; const showFullDiffView = mainView === "thread" && diffOpen; @@ -4053,7 +4106,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", @@ -5744,6 +5797,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); @@ -7107,7 +7163,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 && @@ -7722,7 +7778,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 @@ -7745,8 +7801,8 @@ export function App({ paddingRight: 2, }} > - - {responsiveLayout.showWindowDots ? : null} + + {responsiveLayout.showWindowDots && !isChatMode ? : null} + {isChatMode ? ( + + { + if (activeProjectId) { + openDraftThread(activeProjectId); + } + }} + style={{ + backgroundColor: RGBA.fromHex("#a23b67"), + height: 1, + justifyContent: "center", + alignItems: "center", + paddingTop: 0, + paddingBottom: 0, + }} + > + + + 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} + - + />} - {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 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); @@ -7996,9 +8228,157 @@ export function App({ ) : null} ); - })} + }) : 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 ? PALETTE.composerBorder : PALETTE.border, + paddingLeft: 1, + paddingRight: 1, + marginRight: 1, + backgroundColor: PALETTE.surfaceAlt, + justifyContent: "center", + alignItems: "center", + }} + > + + + { setProfileNameFocused(true); setProfileIconFocused(false); }} + style={{ flexGrow: 1, border: true, borderStyle: "rounded", borderColor: profileNameFocused ? PALETTE.composerBorder : PALETTE.border, backgroundColor: PALETTE.surfaceAlt, justifyContent: "center", paddingLeft: 1 }} + > + { + setNewProfileName(value.slice(0, 50)); + setProfileNameFocused(true); + }} + style={{ + flexGrow: 1, + backgroundColor: PALETTE.surfaceAlt, + textColor: PALETTE.text, + focusedTextColor: PALETTE.text, + focusedBackgroundColor: PALETTE.surfaceAlt, + }} + /> + + + { + 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: newProfileName.trim() ? PALETTE.composerSend : PALETTE.controlActive, + 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} @@ -8062,6 +8443,46 @@ export function App({ backgroundColor: PALETTE.main, }} > + {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)} + /> + { + setOverlayMenu((current) => current === "chat-settings" ? null : "chat-settings"); + }} + /> + + + {/* Row 3: bottom padding, sidebar bg only on right to match icon area */} + + + + + + ) : null} + {isChatMode && responsiveLayout.showSidebar ? null : ( + {isChatMode && responsiveLayout.showSidebar ? null : ( ) : null} + )} + {isChatMode && responsiveLayout.showSidebar ? null : ( restoreDefaultSettings()} /> - ) : mainView === "keybindings" ? null : ( + ) : mainView === "keybindings" ? null : isChatMode ? ( + <> + setTempChatMode((prev) => !prev)} + /> + { + openMainView("settings"); + }} + /> + + ) : ( <> )} + )} + )} @@ -8186,6 +8636,17 @@ export function App({ {mainView === "settings" ? ( <> + setIsChatMode((v) => !v)} + /> + } + /> - {!activeProject && !activeThread && !activeDraftThread ? ( + {isChatMode && (!activeThread || activeThread.messages.length === 0) ? ( + + + {tempChatMode ? ( + + + + + ) : ( + + )} + + + + {[ + { icon: "󰛕", label: "Create" }, + { icon: "󰎕", label: "Explore" }, + { icon: "󰅪", label: "Code" }, + { icon: "󰑴", label: "Learn" }, + ].map((item) => ( + { + syncComposerValueRefSoon(); + setComposer(`${item.label} `); + setTimeout(() => composerRef.current?.focus(), 0); + }} + /> + ))} + + + + {[ + "How does AI work?", + "Are black holes real?", + 'How many Rs are in the word "strawberry"?', + "What is the meaning of life?", + ].map((q, i) => ( + { + e.preventDefault(); + e.stopPropagation(); + syncComposerValueRefSoon(); + setComposer(q); + setTimeout(() => composerRef.current?.focus(), 0); + }} + style={{ + border: i > 0 ? ["top"] : [], + borderColor: PALETTE.divider, + paddingTop: 0, + paddingBottom: 0, + height: i === 0 ? 1 : 2, + alignItems: "flex-start", + justifyContent: "center", + width: "100%", + }} + > + + + ))} + + + ) : !activeProject && !activeThread && !activeDraftThread ? ( ) : ( - + )} + {null} {activePendingApproval ? ( <> @@ -9769,8 +10306,8 @@ export function App({ /> ) : null} - {responsiveLayout.showComposerDividers ? : null} - : null} + {!isChatMode ? - {responsiveLayout.showComposerDividers ? : null} - : null} + {!isChatMode && responsiveLayout.showComposerDividers ? : null} + {!isChatMode ? + /> : null} {activePendingProgress ? ( <> @@ -9852,7 +10389,7 @@ export function App({ )} - {activeProjectId && isGitRepo ? ( + {activeProjectId && isGitRepo && !isChatMode ? ( ) : 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 ? (