Skip to content

feat: add t1chat mode#13

Open
ahzs645 wants to merge 13 commits into
maria-rcks:mainfrom
ahzs645:main
Open

feat: add t1chat mode#13
ahzs645 wants to merge 13 commits into
maria-rcks:mainfrom
ahzs645:main

Conversation

@ahzs645

@ahzs645 ahzs645 commented Apr 7, 2026

Copy link
Copy Markdown

T3 Chat gave us the chat access. T3 Code gave us code access. This brings them together.

What this does

Adds an optional chat mode to the TUI that trades the code-first workflow for a conversational interface, think T3 Chat, but you never have to leave your terminal.

The good stuff

  • Chat mode — flat thread list grouped by time, thread search, New Chat button, centered branding. All the things you'd expect from a proper chat app, rendered in your terminal like nature intended
  • Profiles — because one personality per terminal session was never enough. Create profiles, pick icons, switch between them. Each gets its own threads
  • The T3 Chat look — pink/magenta/lavender theme with dark mode and a "boring mode" for when you're sharing your screen in a meeting
  • Top-right toolbar — temp chat toggle and a settings dropdown with theme switcher, because even terminal apps deserve a little polish
  • Live mode switching — flip between T1 Code and T1 Chat from Settings without restarting. For the indecisive among us
  • Streamlined composer — hides the code-specific tools (git, diffs, plan mode, approval controls) so you can focus on the conversation
demo

Summary by CodeRabbit

  • New Features
    • Added a dedicated Chat mode with a redesigned sidebar, profile-based thread filtering, chat search, onboarding prompts, and chat-specific controls.
    • Introduced a new Boring theme option and updated theme behavior across the UI.
    • Added a t1chat launcher and GitHub Pages deployment for the t1code-demo app.
  • Bug Fixes
    • Improved chat thread, profile, and settings handling for smoother navigation and drafts.
  • Documentation
    • Updated the README with chat-mode build/run instructions and new usage variants.
  • Chores
    • Ignored app build artifacts (apps/*/target) in version control.

google-labs-jules Bot and others added 11 commits April 7, 2026 07:57
- 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>
- 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>
@ahzs645

ahzs645 commented Apr 7, 2026

Copy link
Copy Markdown
Author

Would it make more sense to incorporate this as part of the existing monorepo (since it shares the server, client-core, and TUI infrastructure), or keep it as a separate fork to close this an deprecate the code part... (also let me know if this is not appropriate to make a pr like this)

@maxktz

maxktz commented Apr 8, 2026

Copy link
Copy Markdown

Looks very good!

@amah853 amah853 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I didn't read through everything, but overall, in my opinion, this is absolutely not ready to be merged. First, fix up all of the icons, since they aren't globally recognized icons (I'm on a Mac). Next, make sure that the UI stays consistent between modes (see below).

Lastly, make sure that the coding mode is on by default, not the chat mode.

Comment thread apps/tui/src/profiles.ts

/** Nerd Font icons available for profile selection. */
export const PROFILE_ICON_OPTIONS: { icon: string; label: string }[] = [
{ icon: "󰭹", label: "Chat" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

None of the icons from line R16-R40 are rendering for me -- do they work on your machine? I'm running MacOS 26 on a MacBook Air M5 2026.

I would recommend using emojis, since those are global.

Comment thread apps/tui/src/profiles.ts
export const DEFAULT_PROFILE: Profile = {
id: "default",
name: "Default",
icon: "󰭹",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same thing here, this icon isn't rendering.

Comment thread apps/tui/src/theme.ts
surfaceAlt: "#1f1f1f",
input: "#111111",
surfaceUser: "#202020",
canvas: "#21141e",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why are you modifying the dark palette here? I would leave this as-is and only add the new pink color for chat mode, otherwise I'd leave the t1code theme the same.

export function resolveTuiResponsiveLayout(input: {
viewportColumns: number;
sidebarCollapsedPreference: boolean;
isChatMode?: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You should make sure to leave this as false by default. Remember, the core of t1code is the coding mode.

Comment thread apps/tui/src/ui.tsx
const [tempChatMode, setTempChatMode] = useState(false);
const [sidebarSearchQuery, setSidebarSearchQuery] = useState("");
const [chatProfiles, setChatProfiles] = useState<import("./profiles").Profile[]>([
{ id: "default", name: "Default", icon: "󰭹" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Again, use global emojis here, this icon isn't rendering on my machine.

Comment thread apps/tui/src/ui.tsx
width: responsiveLayout.showSidebar ? responsiveLayout.sidebarWidth : TUI_SIDEBAR_WIDTH,
backgroundColor: sidebarBg,
border: ["right"],
border: isChatMode ? [] : ["right"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Was leaving this blank for isChatMode intended?

Comment thread apps/tui/src/ui.tsx Outdated

type TimeGroup = { label: string; threads: typeof allThreads };
const groups: TimeGroup[] = [
{ label: "Today", threads: [] },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This could be a nice feature to implement globally, not just for the chat mode.

Comment thread apps/tui/src/ui.tsx
style={{
position: "absolute",
top: 4,
right: 1,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We should keep the UI consistent. If you want to move the settings button to the top-right, it needs to stay there for t1code too.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same thing for the keybind control too.

Comment thread apps/tui/src/ui.tsx Outdated
backgroundColor: appSettings.theme === "light" ? PALETTE.controlActive : "transparent",
}}
>
<text content="󰖙" style={{ fg: appSettings.theme === "light" ? PALETTE.text : PALETTE.muted }} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This icon isn't rendering either-- fix.

Comment thread apps/tui/src/ui.tsx Outdated
backgroundColor: appSettings.theme === "system" || !appSettings.theme ? PALETTE.controlActive : "transparent",
}}
>
<text content="󰍹" style={{ fg: appSettings.theme === "system" || !appSettings.theme ? PALETTE.text : PALETTE.muted }} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Again, here.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d69a0f7-71fb-4030-bb3c-90fab545aeef

📥 Commits

Reviewing files that changed from the base of the PR and between 4dfe8b3 and 5e0973a.

📒 Files selected for processing (1)
  • .github/workflows/pages.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/pages.yml

📝 Walkthrough

Walkthrough

This PR adds a Rust/Wasm demo app with GitHub Pages deployment, and introduces a t1chat chat-mode variant of the TUI with profile management, theme changes, responsive layout updates, and chat-specific UI behavior. README documentation is updated to describe the new mode.

Changes

t1code-demo Wasm Application and Pages Deployment

Layer / File(s) Summary
Demo package manifest and dependencies
apps/t1code-demo/Cargo.toml
Adds the t1code-demo Cargo package metadata and Rust dependencies.
Demo HTML shell and styling
apps/t1code-demo/index.html
Adds the demo HTML document with embedded styles, SVG symbols, and overlay markup.
Demo state and rendering pipeline
apps/t1code-demo/src/main.rs
Implements demo state, palettes, layout computation, event handling, renderers, helper utilities, and the entry point.
GitHub Pages deployment workflow
.github/workflows/pages.yml, .gitignore
Adds the Pages workflow, build/deploy jobs, and the ignore rule for Rust target artifacts.

t1chat Chat Mode for TUI

Layer / File(s) Summary
t1chat launcher script and package wiring
apps/tui/bin/t1chat.js, apps/tui/package.json, scripts/dev-tui.ts
Adds the t1chat launcher, registers it as a package binary, and forwards chat-mode into the dev script environment.
Profile data model
apps/tui/src/profiles.ts
Adds the profile type, icon options, default profile, creation helper, and reordering helper.
Boring theme and palette updates
apps/tui/src/theme.ts
Adds the boring theme id and label, updates default palettes and theme details, defines boring theme variants, and caches them.
Responsive layout chat-mode title
apps/tui/src/responsiveLayout.ts
Adds the chat-mode flag to responsive layout input and switches the sidebar title between T1 Chat and T1 Code.
Chat-mode UI state and imports
apps/tui/src/ui.tsx
Adds chat-mode state, imports, overlay menu wiring, responsive layout input, quit text, and thread profile assignment.
Chat-mode sidebar: new chat, search, thread list
apps/tui/src/ui.tsx
Reworks the sidebar for chat mode with a new-chat button, search input, time-grouped thread list, and chat-specific layout behavior.
Chat-mode composer, timeline, and settings UI
apps/tui/src/ui.tsx
Adds the chat onboarding empty state, adjusts composer spacing and controls, adds the mode settings row, and introduces the chat-settings overlay.

README Documentation Updates

Layer / File(s) Summary
README header and t1chat documentation
README.md
Reworks the README header and adds the t1chat chat-mode description and usage instructions.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions
  participant BuildJob
  participant Trunk
  participant DeployJob
  participant GitHubPages
  GitHubActions->>BuildJob: trigger on push/workflow_dispatch
  BuildJob->>Trunk: build apps/t1code-demo with --public-url
  BuildJob->>GitHubPages: upload dist as Pages artifact
  BuildJob->>DeployJob: complete build
  DeployJob->>GitHubPages: deploy-pages@v4
  GitHubPages-->>DeployJob: page_url output
Loading
sequenceDiagram
  participant User
  participant t1chat.js
  participant Bun
  participant ui.tsx
  User->>t1chat.js: run t1chat
  t1chat.js->>t1chat.js: set T1CODE_CHAT_MODE=1
  t1chat.js->>Bun: spawn or import runtime
  Bun->>ui.tsx: start TUI with chat mode env
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding the new t1chat mode.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch main

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/tui/src/ui.tsx (1)

7931-8003: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Range-select can silently include threads hidden by search/profile filtering.

Inside the chat-mode thread list IIFE, a local const allThreads = sortedProjects.flatMap(...) shadows the outer memoized allThreads (defined at ~line 3261) and is unfiltered by the active profile/search query. It's built at line 7933-7939, then profileThreads/filteredThreads are derived from it for what's actually rendered — but handleThreadClick(event, thread.projectId, thread.id, allThreads.map((t) => t.id)) at line 7997-7999 passes the unfiltered list as the ordered-id array used for shift-click range selection (see rangeSelectThread).

Practical impact: with an active search query or non-default profile, shift-click range select can mark threads that aren't even visible in the current view as selected — and since multi-select supports bulk delete via the context menu, a user could delete threads they never saw selected.

🐛 Proposed fix
                               onPress={(event) => {
                                 closeSidebarContextMenu();
                                 handleThreadClick(
                                   event,
                                   thread.projectId,
                                   thread.id,
-                                  allThreads.map((t) => t.id),
+                                  filteredThreads.map((t) => t.id),
                                 );
                               }}

Also consider renaming the local allThreads to avoid shadowing the outer component-scope allThreads.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tui/src/ui.tsx` around lines 7931 - 8003, The chat-mode thread list is
using an unfiltered local allThreads array for shift-click range selection,
which can include threads hidden by the active profile or search filter. Update
the onPress path in the chat-mode IIFE to pass the visible ordered thread IDs
instead of all project threads, ideally using the filtered list derived from
profileThreads/filteredThreads. Also rename the local allThreads inside the IIFE
to avoid shadowing the outer allThreads and keep
rangeSelectThread/handleThreadClick aligned with what is actually rendered.
♻️ Duplicate comments (2)
apps/tui/src/theme.ts (1)

59-103: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Default palette/theme changed for all modes, not just chat — flagged before.

A previous reviewer asked that the default t1code dark palette be left as-is, with new pink/magenta colors scoped to chat mode only. This still replaces DEFAULT_DARK_PALETTE, DEFAULT_THEME_DETAILS, and DEFAULT_LIGHT_PALETTE wholesale — these are the palettes returned for themeId === "default" regardless of chat vs. code mode, so existing T1 Code users get the new pink/magenta look unless a later layer (not in this cohort) explicitly branches theme selection by isChatMode.

#!/bin/bash
# Check whether theme resolution branches on chat mode anywhere in ui.tsx
rg -n -C3 'isChatMode' apps/tui/src/ui.tsx | rg -n -C3 'Theme|theme'

Also applies to: 108-127, 161-196

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tui/src/theme.ts` around lines 59 - 103, The default theme palettes are
being replaced globally, which makes the new pink/magenta styling affect
non-chat modes too. Update the theme selection logic around
DEFAULT_DARK_PALETTE, DEFAULT_LIGHT_PALETTE, and DEFAULT_THEME_DETAILS so the
existing default T1 Code palettes remain unchanged for code mode, and only the
chat-mode path uses the new colors. Use the theme resolution entry points in
theme.ts and any caller branches on isChatMode to keep the new palette scoped
correctly.
apps/tui/src/ui.tsx (1)

8499-8548: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Previously flagged UI-consistency concern remains unaddressed — Keybindings is now unreachable in chat mode.

A prior reviewer explicitly asked that moving Settings/keybind controls to the top-right stay consistent between t1code and chat mode ("Same thing for the keybind control too"). In the current diff, chat mode's top-right toolbar only exposes temp-chat and Settings icons; there is no Keybindings entry point at all in chat mode (code mode's sidebar footer still has both "Settings" and "Keybindings" rows, and the Tab focus-order arrays for mainView !== "thread" never include a way to reach the keybindings view when chat mode's sidebar footer is hidden). This isn't just cosmetic inconsistency — the Keybindings page becomes practically unreachable via the chat-mode UI.

Also applies to: 8612-8635, 11132-11246

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tui/src/ui.tsx` around lines 8499 - 8548, The chat-mode top-right
toolbar is missing the Keybindings entry, making that view unreachable and
inconsistent with the code-mode sidebar controls. Update the chat-mode toolbar
in the same UI region that renders the Settings button so it also exposes a
Keybindings action, and wire it to the existing keybindings overlay/view used
elsewhere in the component. Make sure the focus-order/tab navigation paths that
currently handle non-thread main views also include the Keybindings route when
the chat sidebar footer is hidden, using the existing overlay/menu state and
view-switching logic in ui.tsx.
🧹 Nitpick comments (8)
apps/tui/src/profiles.ts (1)

49-59: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Potential ID collision from Date.now().

Two profiles created within the same millisecond (e.g. same name, rapid succession) get identical IDs. Prefer crypto.randomUUID() or append a random suffix.

♻️ Proposed fix
 export function createProfile(name: string, icon: string): Profile {
   const slug = name
     .toLowerCase()
     .replace(/[^a-z0-9]+/g, "-")
     .replace(/^-|-$/g, "");
   return {
-    id: `${slug}-${Date.now()}`,
+    id: `${slug || "profile"}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
     name,
     icon,
   };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tui/src/profiles.ts` around lines 49 - 59, The profile ID generation in
createProfile currently relies on Date.now(), which can produce duplicate ids
when profiles are created in the same millisecond. Update createProfile to use a
collision-resistant unique suffix such as crypto.randomUUID() or another random
component, while keeping the existing slug-based prefix so the Profile.id
remains readable and unique.
apps/t1code-demo/src/main.rs (4)

858-871: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Dead ..area struct-update in controls Rect (Clippy).

All four fields (x, y, width, height) are already specified, so ..area has no effect.

🧹 Proposed fix
     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
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/t1code-demo/src/main.rs` around lines 858 - 871, The `controls` Rect in
`draw_main_header` uses a struct update with `..area` even though all fields are
already explicitly set, so remove the redundant update and keep the `Rect`
construction fully explicit. Locate the fix in `draw_main_header` alongside the
`top_band` and `controls` layout code, and ensure the `controls` initializer
only contains the fields that actually need to be derived.

Source: Linters/SAST tools


21-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

handle_footer_click is unreachable dead code.

FOOTER_HEIGHT is 0, so layout.footer always has zero height; contains() can never return true for a zero-height rect, meaning handle_mouse's call to handle_footer_click (line 271) never fires, and the column-range branches (sidebar toggle / new chat / temporary chat / thinking mode at Lines 335-339) are unreachable.

Remove handle_footer_click, the footer branch in handle_mouse, and draw_footer's early-return guard if the footer row is no longer part of the design, or restore FOOTER_HEIGHT if it's still needed.

Also applies to: 332-341, 1227-1234

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/t1code-demo/src/main.rs` at line 21, `handle_footer_click` is dead code
because `FOOTER_HEIGHT` is set to zero, so `layout.footer` can never be hit and
the footer-related branches in `handle_mouse` will never execute. Either remove
the unused footer path entirely by deleting `handle_footer_click`, the footer
branch in `handle_mouse`, and the early-return guard in `draw_footer`, or
restore a non-zero `FOOTER_HEIGHT` if the footer interactions are still
intended. Use the symbols `FOOTER_HEIGHT`, `handle_footer_click`,
`handle_mouse`, and `draw_footer` to update the design consistently.

232-237: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Collapse redundant if into the match guard (Clippy).

🧹 Proposed fix
-            KeyCode::Char(c) => {
-                if !key.ctrl && !key.alt {
-                    self.focus = FocusArea::Composer;
-                    self.composer.push(c);
-                }
-            }
+            KeyCode::Char(c) if !key.ctrl && !key.alt => {
+                self.focus = FocusArea::Composer;
+                self.composer.push(c);
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/t1code-demo/src/main.rs` around lines 232 - 237, The `KeyCode::Char(c)`
arm in `main.rs` has a redundant nested condition that Clippy flags; move the
`!key.ctrl && !key.alt` check into the match arm as a guard so the
`KeyCode::Char` branch only matches unmodified character input. Update the
handling in the `KeyCode::Char(c)` match arm to preserve the existing behavior
of setting `self.focus` to `FocusArea::Composer` and calling
`self.composer.push(c)` only when no modifier keys are pressed.

Source: Linters/SAST tools


377-387: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Row-based click hit-testing uses magic numbers duplicated from render order.

handle_settings_view_click's row.saturating_sub(area.y) matches (5, 6, 10, 13, 17) and thread_index_for_sidebar_row's absolute rows (9-12) only work because they happen to mirror the exact line order in draw_settings_view's lines vec and the Layout::vertical heights in draw_sidebar. Any future reordering/insertion of a settings row or sidebar section will silently break click handling (users clicking a row would toggle the wrong setting) with no compiler or test signal.

Consider deriving these indices from shared named constants (or building the row list once and returning (label, value, Option<Action>) tuples consumed by both the renderer and the click handler) so the two stay in sync by construction.

Also applies to: 1637-1645

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/t1code-demo/src/main.rs` around lines 377 - 387, The click hit-testing
in handle_settings_view_click and thread_index_for_sidebar_row is hard-coded to
magic row numbers that mirror draw_settings_view and draw_sidebar render order,
so changes to the UI layout can silently break clicks. Replace the duplicated
numeric offsets with shared named constants or a single source of truth for the
settings/sidebar rows, and have both the renderer and click handlers consume the
same row definitions or action metadata so the mappings stay in sync.
apps/t1code-demo/index.html (2)

133-257: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Overlay icon positions are hardcoded pixel values with no shared source of truth against the Rust layout.

Values like left: 321px, bottom: 37px, and the various top/left offsets for .search-icon, .composer-toolbar-dom, .chat-settings-dom, .model-provider-icons, etc. must stay pixel-perfect in sync with SIDEBAR_WIDTH, COMPOSER_HEIGHT, and the row layout in src/main.rs, but there's no shared constant or generation step tying them together. Any change to the Rust-side layout constants will silently desync these DOM overlays from the canvas-rendered terminal grid.

Given this is a demo, consider at minimum a code comment cross-referencing the exact Rust constants each block of CSS depends on, to reduce the risk of silent drift.

Also applies to: 363-410

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/t1code-demo/index.html` around lines 133 - 257, The overlay positioning
CSS is hardcoded and can drift from the Rust layout, so add explicit
cross-references for each affected block to the exact Rust constants it depends
on. Update the styles around composer-toolbar-dom, chat-settings-dom,
model-provider-icons, and the other overlay selectors to include comments
pointing to the matching layout values in src/main.rs such as SIDEBAR_WIDTH,
COMPOSER_HEIGHT, and the row offsets, so future changes are kept in sync. Keep
the existing pixel values, but document the dependency clearly where each
position block is defined.

6-10: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

External CDN assets lack SRI and pin no version for the full nerd-fonts library.

Both the Fira Code and nerdfonts.com stylesheets are loaded without integrity/crossorigin attributes, and https://www.nerdfonts.com/assets/css/webfont.css pulls the entire icon set (reported at ~1.4MB) from an unversioned URL rather than a pinned/self-hosted subset — a supply-chain risk (no integrity check) and unnecessary payload for the handful of glyphs actually used (search, hourglass, settings, message, bot icons, etc.).

Consider self-hosting only the needed glyphs or pinning to a versioned CDN build (as already done for Fira Code with 6.2.0), and add integrity/crossorigin attributes to both <link> tags.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/t1code-demo/index.html` around lines 6 - 10, The stylesheet links in the
document head are loading external CDN assets without protection and one of them
uses an unpinned full icon bundle. Update the Fira Code and nerdfonts.com <link>
entries to use versioned, trusted sources with integrity and crossorigin
attributes, and replace the full nerdfonts webfont import with a pinned or
self-hosted subset that only includes the icons actually used. Locate the
changes around the existing stylesheet <link> tags in the page head.
apps/tui/src/ui.tsx (1)

7931-7981: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider memoizing the chat thread-list computation.

The flatMap/sort/filter/group-by-time pipeline for the chat sidebar runs inline on every render of App (not wrapped in useMemo, unlike the analogous allThreads/threadsByProject computations at the top of the component). Since App re-renders frequently (composer edits, animation ticks, etc.), wrapping this in useMemo keyed on [sortedProjects, threadsByProject, threadProfileMap, activeProfileId, sidebarSearchQuery] would avoid needless recomputation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tui/src/ui.tsx` around lines 7931 - 7981, The chat sidebar thread-list
pipeline inside App is recomputing on every render, so move the
flatMap/sort/filter/grouping logic into a useMemo. Memoize the computation that
builds allThreads, profileThreads, filteredThreads, and the time groups, and key
it off sortedProjects, threadsByProject, threadProfileMap, activeProfileId, and
sidebarSearchQuery so the JSX only re-renders the derived list when inputs
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/pages.yml:
- Around line 26-27: The checkout step in the pages workflow is leaving GitHub
credentials on disk even though the build job only compiles the wasm demo and
never pushes anything. Update the actions/checkout usage in the workflow to
disable credential persistence by setting persist-credentials to false on the
existing checkout step, so the build job does not retain unnecessary auth
tokens.
- Around line 42-44: The Build demo step in the pages workflow is interpolating
github.event.repository.name directly inside the trunk build command, which
matches the flagged shell-injection pattern. Update the workflow so the value is
passed into the step via an environment variable, then reference that variable
in the run command in the Build demo job instead of using direct template
expansion.
- Around line 12-15: The workflow-level permissions in pages.yml are too broad
because id-token: write and pages: write are inherited by the build job; move
those permissions into the deploy job only and keep the top-level permissions
limited to read-only access. Update the workflow’s permissions block and the
deploy job definition so the build job no longer receives deployment privileges.

In `@apps/tui/src/ui.tsx`:
- Around line 11216-11230: The Boring Mode toggle in ui.tsx is hardcoding
"default" when disabling the mode, which discards the user’s previously selected
theme preset. Update the toggle handler in the Boring Mode box to remember the
last non-boring tuiThemeId before switching to BORING_THEME_ID, then restore
that saved value when toggling off instead of always setting "default". Use the
existing tuiThemeId state and BORING_THEME_ID symbol to keep the prior preset
intact across toggles.
- Around line 9465-9555: The quick-fill handlers in the chat intro UI are
updating only React state and not the actual composer textarea, so the visible
input and `readComposerValue()` stay stale. Update `ChatCategoryButton.onPress`
and the suggested-question `onMouseDown` flow to use the same
`resetComposerTextarea(...)` helper used elsewhere, so `composerResetKey` is
bumped and the textarea is re-seeded before focusing. Keep the fix scoped to the
chat empty-state section in `ui.tsx` and preserve the existing `composerRef`
focus behavior.
- Around line 1321-1354: ChatCategoryButton is bypassing the theme system by
using hardcoded pink and white hex values, which prevents Boring Mode from
recoloring the UI. Update ChatCategoryButton to use existing palette tokens such
as PALETTE.accent, PALETTE.text, and the appropriate themed text color instead
of literal hex values, and apply the same replacement pattern to the other
affected button/label components in this diff (including the New Chat button and
Temporary chat onboarding label) so all hover and foreground styles follow the
active palette.
- Around line 7824-7882: The New Chat action in the chat sidebar currently
depends on activeProjectId, so it does nothing on first run when no project
exists. Update the New Chat onMouseDown handler in ui.tsx to use the same
project bootstrap flow as sendPrompt() and createProject(process.cwd()) before
calling openDraftThread, or otherwise disable the button until a project is
available. Keep the fix localized to the New Chat box behavior and reuse the
existing openDraftThread/createProject logic.
- Around line 2840-2853: Persist the chat/profile state instead of keeping it
only in local React state: `isChatMode`, `chatProfiles`, `activeProfileId`, and
`threadProfileMap` in `ui.tsx` should be loaded from and saved to `TuiPrefs` so
T1 Chat/T1 Code mode, custom profiles, and thread bindings survive restarts.
Update the `TuiPrefs` shape and the corresponding prefs load/save flow around
these state hooks, and make sure the profile management paths keep writing
changes back through the existing prefs persistence logic.

In `@README.md`:
- Around line 32-36: The setup instructions have a repository/directory
mismatch: after cloning via git clone in the “Develop from source” snippets, the
next step should cd into the directory created by that clone, not the old t1code
name. Update both occurrences in the README so the cd command matches the cloned
t1chat repository name and keeps the install/dev steps consistent.

---

Outside diff comments:
In `@apps/tui/src/ui.tsx`:
- Around line 7931-8003: The chat-mode thread list is using an unfiltered local
allThreads array for shift-click range selection, which can include threads
hidden by the active profile or search filter. Update the onPress path in the
chat-mode IIFE to pass the visible ordered thread IDs instead of all project
threads, ideally using the filtered list derived from
profileThreads/filteredThreads. Also rename the local allThreads inside the IIFE
to avoid shadowing the outer allThreads and keep
rangeSelectThread/handleThreadClick aligned with what is actually rendered.

---

Duplicate comments:
In `@apps/tui/src/theme.ts`:
- Around line 59-103: The default theme palettes are being replaced globally,
which makes the new pink/magenta styling affect non-chat modes too. Update the
theme selection logic around DEFAULT_DARK_PALETTE, DEFAULT_LIGHT_PALETTE, and
DEFAULT_THEME_DETAILS so the existing default T1 Code palettes remain unchanged
for code mode, and only the chat-mode path uses the new colors. Use the theme
resolution entry points in theme.ts and any caller branches on isChatMode to
keep the new palette scoped correctly.

In `@apps/tui/src/ui.tsx`:
- Around line 8499-8548: The chat-mode top-right toolbar is missing the
Keybindings entry, making that view unreachable and inconsistent with the
code-mode sidebar controls. Update the chat-mode toolbar in the same UI region
that renders the Settings button so it also exposes a Keybindings action, and
wire it to the existing keybindings overlay/view used elsewhere in the
component. Make sure the focus-order/tab navigation paths that currently handle
non-thread main views also include the Keybindings route when the chat sidebar
footer is hidden, using the existing overlay/menu state and view-switching logic
in ui.tsx.

---

Nitpick comments:
In `@apps/t1code-demo/index.html`:
- Around line 133-257: The overlay positioning CSS is hardcoded and can drift
from the Rust layout, so add explicit cross-references for each affected block
to the exact Rust constants it depends on. Update the styles around
composer-toolbar-dom, chat-settings-dom, model-provider-icons, and the other
overlay selectors to include comments pointing to the matching layout values in
src/main.rs such as SIDEBAR_WIDTH, COMPOSER_HEIGHT, and the row offsets, so
future changes are kept in sync. Keep the existing pixel values, but document
the dependency clearly where each position block is defined.
- Around line 6-10: The stylesheet links in the document head are loading
external CDN assets without protection and one of them uses an unpinned full
icon bundle. Update the Fira Code and nerdfonts.com <link> entries to use
versioned, trusted sources with integrity and crossorigin attributes, and
replace the full nerdfonts webfont import with a pinned or self-hosted subset
that only includes the icons actually used. Locate the changes around the
existing stylesheet <link> tags in the page head.

In `@apps/t1code-demo/src/main.rs`:
- Around line 858-871: The `controls` Rect in `draw_main_header` uses a struct
update with `..area` even though all fields are already explicitly set, so
remove the redundant update and keep the `Rect` construction fully explicit.
Locate the fix in `draw_main_header` alongside the `top_band` and `controls`
layout code, and ensure the `controls` initializer only contains the fields that
actually need to be derived.
- Line 21: `handle_footer_click` is dead code because `FOOTER_HEIGHT` is set to
zero, so `layout.footer` can never be hit and the footer-related branches in
`handle_mouse` will never execute. Either remove the unused footer path entirely
by deleting `handle_footer_click`, the footer branch in `handle_mouse`, and the
early-return guard in `draw_footer`, or restore a non-zero `FOOTER_HEIGHT` if
the footer interactions are still intended. Use the symbols `FOOTER_HEIGHT`,
`handle_footer_click`, `handle_mouse`, and `draw_footer` to update the design
consistently.
- Around line 232-237: The `KeyCode::Char(c)` arm in `main.rs` has a redundant
nested condition that Clippy flags; move the `!key.ctrl && !key.alt` check into
the match arm as a guard so the `KeyCode::Char` branch only matches unmodified
character input. Update the handling in the `KeyCode::Char(c)` match arm to
preserve the existing behavior of setting `self.focus` to `FocusArea::Composer`
and calling `self.composer.push(c)` only when no modifier keys are pressed.
- Around line 377-387: The click hit-testing in handle_settings_view_click and
thread_index_for_sidebar_row is hard-coded to magic row numbers that mirror
draw_settings_view and draw_sidebar render order, so changes to the UI layout
can silently break clicks. Replace the duplicated numeric offsets with shared
named constants or a single source of truth for the settings/sidebar rows, and
have both the renderer and click handlers consume the same row definitions or
action metadata so the mappings stay in sync.

In `@apps/tui/src/profiles.ts`:
- Around line 49-59: The profile ID generation in createProfile currently relies
on Date.now(), which can produce duplicate ids when profiles are created in the
same millisecond. Update createProfile to use a collision-resistant unique
suffix such as crypto.randomUUID() or another random component, while keeping
the existing slug-based prefix so the Profile.id remains readable and unique.

In `@apps/tui/src/ui.tsx`:
- Around line 7931-7981: The chat sidebar thread-list pipeline inside App is
recomputing on every render, so move the flatMap/sort/filter/grouping logic into
a useMemo. Memoize the computation that builds allThreads, profileThreads,
filteredThreads, and the time groups, and key it off sortedProjects,
threadsByProject, threadProfileMap, activeProfileId, and sidebarSearchQuery so
the JSX only re-renders the derived list when inputs change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 255c581c-6c3a-478d-8d85-1e92a173c717

📥 Commits

Reviewing files that changed from the base of the PR and between 0b7b2c0 and 4dfe8b3.

⛔ Files ignored due to path filters (3)
  • apps/t1code-demo/Cargo.lock is excluded by !**/*.lock
  • assets/repo/t1chat-preview.png is excluded by !**/*.png
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • .github/workflows/pages.yml
  • .gitignore
  • README.md
  • apps/t1code-demo/Cargo.toml
  • apps/t1code-demo/index.html
  • apps/t1code-demo/src/main.rs
  • apps/tui/bin/t1chat.js
  • apps/tui/package.json
  • apps/tui/src/profiles.ts
  • apps/tui/src/responsiveLayout.ts
  • apps/tui/src/theme.ts
  • apps/tui/src/ui.tsx
  • scripts/dev-tui.ts

Comment on lines +12 to +15
permissions:
contents: read
id-token: write
pages: write

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Scope permissions to the job that needs them.

id-token: write and pages: write are declared at the workflow level, granting them to the build job as well, which doesn't need them. Move these to the deploy job only.

🔒️ Proposed fix
 permissions:
   contents: read
-  id-token: write
-  pages: write
 
 concurrency:
   group: github-pages
   cancel-in-progress: false
 
 jobs:
   build:
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
   deploy:
     needs: build
     runs-on: ubuntu-latest
+    permissions:
+      id-token: write
+      pages: write
     environment:
       name: github-pages
🧰 Tools
🪛 zizmor (1.26.1)

[error] 14-14: overly broad permissions (excessive-permissions): id-token: write is overly broad at the workflow level

(excessive-permissions)


[error] 15-15: overly broad permissions (excessive-permissions): pages: write is overly broad at the workflow level

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pages.yml around lines 12 - 15, The workflow-level
permissions in pages.yml are too broad because id-token: write and pages: write
are inherited by the build job; move those permissions into the deploy job only
and keep the top-level permissions limited to read-only access. Update the
workflow’s permissions block and the deploy job definition so the build job no
longer receives deployment privileges.

Source: Linters/SAST tools

Comment on lines +26 to +27
- name: Check out repository
uses: actions/checkout@v5

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Set persist-credentials: false on checkout.

The build job only compiles the wasm demo and never pushes; persisting the checkout token is unnecessary credential exposure.

🔒️ Proposed fix
       - name: Check out repository
         uses: actions/checkout@v5
+        with:
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Check out repository
uses: actions/checkout@v5
- name: Check out repository
uses: actions/checkout@v5
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 26-27: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pages.yml around lines 26 - 27, The checkout step in the
pages workflow is leaving GitHub credentials on disk even though the build job
only compiles the wasm demo and never pushes anything. Update the
actions/checkout usage in the workflow to disable credential persistence by
setting persist-credentials to false on the existing checkout step, so the build
job does not retain unnecessary auth tokens.

Source: Linters/SAST tools

Comment on lines +42 to +44
- name: Build demo
working-directory: apps/t1code-demo
run: trunk build --release --public-url "/${{ github.event.repository.name }}/"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid direct template expansion inside run:.

${{ github.event.repository.name }} is interpolated directly into a shell command. Pass it through an environment variable instead to avoid the injection pattern flagged by static analysis.

🔒️ Proposed fix
       - name: Build demo
         working-directory: apps/t1code-demo
-        run: trunk build --release --public-url "/${{ github.event.repository.name }}/"
+        env:
+          REPO_NAME: ${{ github.event.repository.name }}
+        run: trunk build --release --public-url "/${REPO_NAME}/"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Build demo
working-directory: apps/t1code-demo
run: trunk build --release --public-url "/${{ github.event.repository.name }}/"
- name: Build demo
working-directory: apps/t1code-demo
env:
REPO_NAME: ${{ github.event.repository.name }}
run: trunk build --release --public-url "/${REPO_NAME}/"
🧰 Tools
🪛 zizmor (1.26.1)

[error] 44-44: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pages.yml around lines 42 - 44, The Build demo step in the
pages workflow is interpolating github.event.repository.name directly inside the
trunk build command, which matches the flagged shell-injection pattern. Update
the workflow so the value is passed into the step via an environment variable,
then reference that variable in the run command in the Build demo job instead of
using direct template expansion.

Source: Linters/SAST tools

Comment thread apps/tui/src/ui.tsx
Comment on lines +1321 to +1354
function ChatCategoryButton(props: { icon: string; label: string; onPress: () => void }) {
const [hoveredCategory, setHoveredCategory] = useState(false);
return (
<box
onMouseOver={() => 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,
}}
>
<text
content={props.icon}
style={{ fg: hoveredCategory ? RGBA.fromHex("#ffffff") : PALETTE.text, marginRight: 1 }}
/>
<text
content={props.label}
style={{ fg: hoveredCategory ? RGBA.fromHex("#ffffff") : PALETTE.text }}
/>
</box>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Hardcoded pink hex bypasses the theme system, undermining "Boring Mode".

ChatCategoryButton uses RGBA.fromHex("#a23b67")/"#ffffff" directly instead of PALETTE.accent/PALETTE.text. Sibling code in this same diff (e.g. the temp-chat toggle icon at ~line 8521/8621) correctly uses PALETTE.accent, so this is likely an oversight rather than intentional. The same hardcoded hex recurs in the "New Chat" button (~line 7833) and the "Temporary chat" onboarding label (~lines 9482-9487).

Since "Boring Mode" (added in this PR) works by swapping the active palette, any UI element using a literal hex value instead of a palette token will stay pink even when Boring Mode is enabled — defeating the feature's purpose.

🎨 Proposed fix
-        backgroundColor: hoveredCategory ? RGBA.fromHex("`#a23b67`") : PALETTE.surfaceAlt,
+        backgroundColor: hoveredCategory ? PALETTE.accent : PALETTE.surfaceAlt,
...
-        borderColor: hoveredCategory ? RGBA.fromHex("`#a23b67`") : PALETTE.border,
+        borderColor: hoveredCategory ? PALETTE.accent : PALETTE.border,
...
-        style={{ fg: hoveredCategory ? RGBA.fromHex("`#ffffff`") : PALETTE.text, marginRight: 1 }}
+        style={{ fg: hoveredCategory ? PALETTE.text : PALETTE.text, marginRight: 1 }}

(apply equivalent replacement at the "New Chat" button and "Temporary chat" label)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function ChatCategoryButton(props: { icon: string; label: string; onPress: () => void }) {
const [hoveredCategory, setHoveredCategory] = useState(false);
return (
<box
onMouseOver={() => 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,
}}
>
<text
content={props.icon}
style={{ fg: hoveredCategory ? RGBA.fromHex("#ffffff") : PALETTE.text, marginRight: 1 }}
/>
<text
content={props.label}
style={{ fg: hoveredCategory ? RGBA.fromHex("#ffffff") : PALETTE.text }}
/>
</box>
);
}
function ChatCategoryButton(props: { icon: string; label: string; onPress: () => void }) {
const [hoveredCategory, setHoveredCategory] = useState(false);
return (
<box
onMouseOver={() => setHoveredCategory(true)}
onMouseOut={() => setHoveredCategory(false)}
onMouseDown={props.onPress}
style={{
backgroundColor: hoveredCategory ? PALETTE.accent : PALETTE.surfaceAlt,
paddingLeft: 2,
paddingRight: 2,
paddingTop: 0,
paddingBottom: 0,
marginRight: 1,
marginBottom: 1,
flexDirection: "row",
alignItems: "center",
border: true,
borderStyle: "rounded",
borderColor: hoveredCategory ? PALETTE.accent : PALETTE.border,
}}
>
<text
content={props.icon}
style={{ fg: hoveredCategory ? PALETTE.text : PALETTE.text, marginRight: 1 }}
/>
<text
content={props.label}
style={{ fg: hoveredCategory ? PALETTE.text : PALETTE.text }}
/>
</box>
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tui/src/ui.tsx` around lines 1321 - 1354, ChatCategoryButton is
bypassing the theme system by using hardcoded pink and white hex values, which
prevents Boring Mode from recoloring the UI. Update ChatCategoryButton to use
existing palette tokens such as PALETTE.accent, PALETTE.text, and the
appropriate themed text color instead of literal hex values, and apply the same
replacement pattern to the other affected button/label components in this diff
(including the New Chat button and Temporary chat onboarding label) so all hover
and foreground styles follow the active palette.

Comment thread apps/tui/src/ui.tsx
Comment on lines +2840 to +2853

const [isChatMode, setIsChatMode] = useState(process.env.T1CODE_CHAT_MODE === "1");
const [tempChatMode, setTempChatMode] = useState(false);
const [sidebarSearchQuery, setSidebarSearchQuery] = useState("");
const [chatProfiles, setChatProfiles] = useState<import("./profiles").Profile[]>([
{ id: "default", name: "Default", icon: "󰭹" },
]);
const [activeProfileId, setActiveProfileId] = useState("default");
const [threadProfileMap, setThreadProfileMap] = useState<Record<string, string>>({});
const [showProfileCreate, setShowProfileCreate] = useState(false);
const [newProfileName, setNewProfileName] = useState("");
const [newProfileIconIndex, setNewProfileIconIndex] = useState(0);
const [profileNameFocused, setProfileNameFocused] = useState(false);
const [profileIconFocused, setProfileIconFocused] = useState(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether TuiPrefs / readPrefs / writePrefs include chat-mode fields elsewhere
rg -n "chatProfiles|threadProfileMap|activeProfileId|isChatMode" apps/tui/src -g '*.ts' -g '*.tsx'

Repository: maria-rcks/t1code

Length of output: 3954


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Relevant symbols and persistence hooks =="
rg -n "readPrefs|writePrefs|TuiPrefs|chatProfiles|threadProfileMap|activeProfileId|isChatMode|tempChatMode|Profile" apps/tui/src -g '*.ts' -g '*.tsx'

echo
echo "== ui.tsx persistence-related regions =="
sed -n '2940,3065p' apps/tui/src/ui.tsx
echo
sed -n '3188,3275p' apps/tui/src/ui.tsx
echo
sed -n '8360,8435p' apps/tui/src/ui.tsx
echo
sed -n '5788,5820p' apps/tui/src/ui.tsx

echo
echo "== Prefs definitions =="
fd -a "prefs" apps/tui/src || true
rg -n "interface .*Prefs|type .*Prefs|readPrefs|writePrefs|localStorage|file.*prefs|persist" apps/tui/src -g '*.ts' -g '*.tsx'

Repository: maria-rcks/t1code

Length of output: 24077


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== prefs.ts =="
cat -n apps/tui/src/prefs.ts | sed -n '1,220p'

echo
echo "== prefs tests =="
cat -n apps/tui/src/prefs.test.ts | sed -n '1,240p'

echo
echo "== profiles.ts =="
cat -n apps/tui/src/profiles.ts | sed -n '1,220p'

echo
echo "== any persisted chat/profile fields elsewhere =="
rg -n "chatProfiles|threadProfileMap|activeProfileId|isChatMode|tempChatMode|selectedProjectId|selectedThreadId|composerDraftsByThreadId|draftThreadsByProjectId" apps/tui/src -g '*.ts' -g '*.tsx'

Repository: maria-rcks/t1code

Length of output: 25546


Persist chat profiles and mode in TuiPrefs
isChatMode, chatProfiles, activeProfileId, and threadProfileMap are still local state, and TuiPrefs has no fields for them, so T1 Chat/T1 Code toggles and custom profiles/assignments disappear on restart. Wire them into the prefs load/save path so chat profiles and per-thread bindings survive relaunches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tui/src/ui.tsx` around lines 2840 - 2853, Persist the chat/profile state
instead of keeping it only in local React state: `isChatMode`, `chatProfiles`,
`activeProfileId`, and `threadProfileMap` in `ui.tsx` should be loaded from and
saved to `TuiPrefs` so T1 Chat/T1 Code mode, custom profiles, and thread
bindings survive restarts. Update the `TuiPrefs` shape and the corresponding
prefs load/save flow around these state hooks, and make sure the profile
management paths keep writing changes back through the existing prefs
persistence logic.

Comment thread apps/tui/src/ui.tsx
Comment on lines +7824 to +7882
{isChatMode ? (
<box style={{ paddingLeft: 1, paddingRight: 1, flexDirection: "column" }}>
<box
onMouseDown={() => {
if (activeProjectId) {
openDraftThread(activeProjectId);
}
}}
style={{
backgroundColor: RGBA.fromHex("#a23b67"),
height: 1,
justifyContent: "center",
alignItems: "center",
paddingTop: 0,
paddingBottom: 0,
}}
>
<text content="New Chat" style={{ fg: "#ffffff" }} />
</box>
<box
onMouseDown={() => setFocusArea("projects")}
style={{
height: 1,
flexDirection: "row",
alignItems: "center",
marginTop: 1,
paddingBottom: 0,
position: "relative",
}}
>
<text
content="󰍉"
style={{ fg: sidebarSearchQuery ? PALETTE.muted : PALETTE.text, marginRight: 1 }}
/>
{sidebarSearchQuery ? null : (
<text
content="Search your threads..."
style={{ fg: PALETTE.text, position: "absolute", left: 2 }}
/>
)}
<input
value={sidebarSearchQuery}
placeholder=""
cursorColor={PALETTE.muted}
onInput={(value) => setSidebarSearchQuery(value)}
style={{
flexGrow: 1,
backgroundColor: sidebarBg,
textColor: PALETTE.muted,
focusedTextColor: PALETTE.muted,
focusedBackgroundColor: sidebarBg,
}}
/>
</box>
<box style={{ height: 1, marginTop: 1, paddingLeft: 0, paddingRight: 0 }}>
<text content={"─".repeat(TUI_SIDEBAR_WIDTH - 2)} style={{ fg: PALETTE.text }} />
</box>
</box>
) : null}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first
ast-grep outline apps/tui/src/ui.tsx --view expanded

# Find the relevant symbols and nearby usages
rg -n "sendPrompt|openDraftThread|createProject|activeProjectId|isChatMode|New Chat" apps/tui/src/ui.tsx

# Read the relevant slices around the button and sendPrompt implementation
python3 - <<'PY'
from pathlib import Path
p = Path("apps/tui/src/ui.tsx")
lines = p.read_text().splitlines()
for start,end in [(7760,7905),(1,2600)]:
    print(f"\n--- {start}-{end} ---")
    for i in range(start, min(end, len(lines))+1):
        print(f"{i:5d}: {lines[i-1]}")
PY

Repository: maria-rcks/t1code

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focus on the state that defines activeProjectId and the paths mentioned in the comment.
sed -n '3240,3325p' apps/tui/src/ui.tsx
sed -n '5710,5815p' apps/tui/src/ui.tsx
sed -n '6160,6255p' apps/tui/src/ui.tsx

# Look for any other chat-mode/project bootstrap paths.
rg -n "createProject\\(|openDraftThread\\(|activeProjectId\\b|isChatMode\\b|shouldApplyWelcomeBootstrapSelection|process\\.cwd\\(\\)" apps/tui/src/ui.tsx

Repository: maria-rcks/t1code

Length of output: 15478


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '3110,3165p' apps/tui/src/ui.tsx
sed -n '5975,6050p' apps/tui/src/ui.tsx
sed -n '6215,6245p' apps/tui/src/ui.tsx
sed -n '4608,4645p' apps/tui/src/ui.tsx

Repository: maria-rcks/t1code

Length of output: 6919


"New Chat" still no-ops when no project exists. apps/tui/src/ui.tsx:7826-7831 only opens a draft thread if activeProjectId is set, so first-run chat mode has a dead button even though sendPrompt() already creates a project with createProject(process.cwd()). Make the button follow the same bootstrap path or disable it until a project exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tui/src/ui.tsx` around lines 7824 - 7882, The New Chat action in the
chat sidebar currently depends on activeProjectId, so it does nothing on first
run when no project exists. Update the New Chat onMouseDown handler in ui.tsx to
use the same project bootstrap flow as sendPrompt() and
createProject(process.cwd()) before calling openDraftThread, or otherwise
disable the button until a project is available. Keep the fix localized to the
New Chat box behavior and reuse the existing openDraftThread/createProject
logic.

Comment thread apps/tui/src/ui.tsx
Comment on lines +9465 to +9555
{isChatMode && (!activeThread || activeThread.messages.length === 0) ? (
<box
style={{
flexGrow: 1,
flexDirection: "column",
justifyContent: "flex-start",
alignItems: "flex-start",
paddingLeft: 2,
paddingRight: 2,
paddingTop: 4,
}}
>
<box style={{ flexDirection: "row", marginBottom: 2 }}>
{tempChatMode ? (
<box style={{ flexDirection: "row", alignItems: "center" }}>
<text
content="󰔟"
style={{ fg: RGBA.fromHex("#a23b67"), marginRight: 1 }}
/>
<text
content="Temporary chat"
style={{ fg: RGBA.fromHex("#a23b67") }}
/>
</box>
) : (
<text content="How can I help you?" style={{ fg: PALETTE.text }} />
)}
</box>

<box
style={{
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "flex-start",
marginBottom: 1,
}}
>
{[
{ icon: "󰛕", label: "Create" },
{ icon: "󰎕", label: "Explore" },
{ icon: "󰅪", label: "Code" },
{ icon: "󰑴", label: "Learn" },
].map((item) => (
<ChatCategoryButton
key={item.label}
icon={item.icon}
label={item.label}
onPress={() => {
syncComposerValueRefSoon();
setComposer(`${item.label} `);
setTimeout(() => composerRef.current?.focus(), 0);
}}
/>
))}
</box>

<box
style={{ flexDirection: "column", width: "100%", alignItems: "flex-start" }}
>
{[
"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) => (
<box
key={q}
onMouseDown={(e) => {
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%",
}}
>
<text content={q} style={{ fg: PALETTE.muted }} />
</box>
))}
</box>
</box>
) : !activeProject && !activeThread && !activeDraftThread ? (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Quick-fill buttons don't actually update the composer textarea.

ChatCategoryButton.onPress and the suggested-question row onMouseDown both call setComposer(...) directly. However, the <textarea> (line ~10142-10146) is uncontrolled — it uses initialValue={composer} and is only remounted/re-seeded when composerResetKey changes (see key={composerResetKey}). Every other place in this file that needs to programmatically set the composer's visible content uses resetComposerTextarea(...), which also bumps composerResetKey.

Because these two handlers skip that helper, composer React state updates but the actual textarea (and hence composerRef.current?.plainText, which readComposerValue()/sendPrompt() prioritize) stays unchanged — clicking a category button or suggested question will not visibly populate the composer, and sending afterward would send stale/empty text.

🐛 Proposed fix
                             onPress={() => {
                               syncComposerValueRefSoon();
-                              setComposer(`${item.label} `);
+                              resetComposerTextarea(`${item.label} `);
                               setTimeout(() => composerRef.current?.focus(), 0);
                             }}
                             onMouseDown={(e) => {
                               e.preventDefault();
                               e.stopPropagation();
                               syncComposerValueRefSoon();
-                              setComposer(q);
+                              resetComposerTextarea(q);
                               setTimeout(() => composerRef.current?.focus(), 0);
                             }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{isChatMode && (!activeThread || activeThread.messages.length === 0) ? (
<box
style={{
flexGrow: 1,
flexDirection: "column",
justifyContent: "flex-start",
alignItems: "flex-start",
paddingLeft: 2,
paddingRight: 2,
paddingTop: 4,
}}
>
<box style={{ flexDirection: "row", marginBottom: 2 }}>
{tempChatMode ? (
<box style={{ flexDirection: "row", alignItems: "center" }}>
<text
content="󰔟"
style={{ fg: RGBA.fromHex("#a23b67"), marginRight: 1 }}
/>
<text
content="Temporary chat"
style={{ fg: RGBA.fromHex("#a23b67") }}
/>
</box>
) : (
<text content="How can I help you?" style={{ fg: PALETTE.text }} />
)}
</box>
<box
style={{
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "flex-start",
marginBottom: 1,
}}
>
{[
{ icon: "󰛕", label: "Create" },
{ icon: "󰎕", label: "Explore" },
{ icon: "󰅪", label: "Code" },
{ icon: "󰑴", label: "Learn" },
].map((item) => (
<ChatCategoryButton
key={item.label}
icon={item.icon}
label={item.label}
onPress={() => {
syncComposerValueRefSoon();
setComposer(`${item.label} `);
setTimeout(() => composerRef.current?.focus(), 0);
}}
/>
))}
</box>
<box
style={{ flexDirection: "column", width: "100%", alignItems: "flex-start" }}
>
{[
"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) => (
<box
key={q}
onMouseDown={(e) => {
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%",
}}
>
<text content={q} style={{ fg: PALETTE.muted }} />
</box>
))}
</box>
</box>
) : !activeProject && !activeThread && !activeDraftThread ? (
{isChatMode && (!activeThread || activeThread.messages.length === 0) ? (
<box
style={{
flexGrow: 1,
flexDirection: "column",
justifyContent: "flex-start",
alignItems: "flex-start",
paddingLeft: 2,
paddingRight: 2,
paddingTop: 4,
}}
>
<box style={{ flexDirection: "row", marginBottom: 2 }}>
{tempChatMode ? (
<box style={{ flexDirection: "row", alignItems: "center" }}>
<text
content="󰔟"
style={{ fg: RGBA.fromHex("`#a23b67`"), marginRight: 1 }}
/>
<text
content="Temporary chat"
style={{ fg: RGBA.fromHex("`#a23b67`") }}
/>
</box>
) : (
<text content="How can I help you?" style={{ fg: PALETTE.text }} />
)}
</box>
<box
style={{
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "flex-start",
marginBottom: 1,
}}
>
{[
{ icon: "󰛕", label: "Create" },
{ icon: "󰎕", label: "Explore" },
{ icon: "󰅪", label: "Code" },
{ icon: "󰑴", label: "Learn" },
].map((item) => (
<ChatCategoryButton
key={item.label}
icon={item.icon}
label={item.label}
onPress={() => {
syncComposerValueRefSoon();
resetComposerTextarea(`${item.label} `);
setTimeout(() => composerRef.current?.focus(), 0);
}}
/>
))}
</box>
<box
style={{ flexDirection: "column", width: "100%", alignItems: "flex-start" }}
>
{[
"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) => (
<box
key={q}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
syncComposerValueRefSoon();
resetComposerTextarea(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%",
}}
>
<text content={q} style={{ fg: PALETTE.muted }} />
</box>
))}
</box>
</box>
) : !activeProject && !activeThread && !activeDraftThread ? (
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tui/src/ui.tsx` around lines 9465 - 9555, The quick-fill handlers in the
chat intro UI are updating only React state and not the actual composer
textarea, so the visible input and `readComposerValue()` stay stale. Update
`ChatCategoryButton.onPress` and the suggested-question `onMouseDown` flow to
use the same `resetComposerTextarea(...)` helper used elsewhere, so
`composerResetKey` is bumped and the textarea is re-seeded before focusing. Keep
the fix scoped to the chat empty-state section in `ui.tsx` and preserve the
existing `composerRef` focus behavior.

Comment thread apps/tui/src/ui.tsx
Comment on lines +11216 to +11230
<box
onMouseDown={() => {
setTuiThemeId((current) =>
current === BORING_THEME_ID ? "default" : BORING_THEME_ID,
);
}}
style={{ height: 1, flexDirection: "row", alignItems: "center" }}
>
<text content="󰏘" style={{ fg: PALETTE.muted, marginRight: 1 }} />
<text content="Boring Mode" style={{ fg: PALETTE.text, flexGrow: 1 }} />
<text
content={tuiThemeId === BORING_THEME_ID ? "●" : "○"}
style={{ fg: tuiThemeId === BORING_THEME_ID ? PALETTE.accent : PALETTE.subtle }}
/>
</box>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

"Boring Mode" toggle discards the previously selected theme preset.

setTuiThemeId((current) => current === BORING_THEME_ID ? "default" : BORING_THEME_ID) hardcodes "default" when turning Boring Mode off. If the user had a different tuiThemeId preset selected before enabling Boring Mode (e.g. a "Terminal Match" preset, per TUI_THEME_OPTIONS), that choice is silently lost and replaced with "default" instead of being restored.

Track the prior non-boring tuiThemeId (e.g. in a ref) before switching to BORING_THEME_ID, and restore it on toggle-off instead of hardcoding "default".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tui/src/ui.tsx` around lines 11216 - 11230, The Boring Mode toggle in
ui.tsx is hardcoding "default" when disabling the mode, which discards the
user’s previously selected theme preset. Update the toggle handler in the Boring
Mode box to remember the last non-boring tuiThemeId before switching to
BORING_THEME_ID, then restore that saved value when toggling off instead of
always setting "default". Use the existing tuiThemeId state and BORING_THEME_ID
symbol to keep the prior preset intact across toggles.

Comment thread README.md
Comment on lines +32 to 36
git clone https://github.com/ahzs645/t1chat.git
cd t1code
bun install
bun dev:tui
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

cd t1code after cloning t1chat.git — directory name mismatch.

Both "Develop from source" snippets do git clone https://github.com/ahzs645/t1chat.git followed by cd t1code. Cloning that URL creates a t1chat directory (matching the repo name), not t1code, so following these instructions literally will fail with "no such file or directory."

📝 Proposed fix
 git clone https://github.com/ahzs645/t1chat.git
-cd t1code
+cd t1chat
 bun install
 bun dev:tui

(apply the same fix to the second occurrence)

Also applies to: 73-77

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 32 - 36, The setup instructions have a
repository/directory mismatch: after cloning via git clone in the “Develop from
source” snippets, the next step should cd into the directory created by that
clone, not the old t1code name. Update both occurrences in the README so the cd
command matches the cloned t1chat repository name and keeps the install/dev
steps consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants