feat: add t1chat mode#13
Conversation
- 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>
feat: Add t1chat mode
…rdering functionality
|
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) |
|
Looks very good! |
amah853
left a comment
There was a problem hiding this comment.
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.
|
|
||
| /** Nerd Font icons available for profile selection. */ | ||
| export const PROFILE_ICON_OPTIONS: { icon: string; label: string }[] = [ | ||
| { icon: "", label: "Chat" }, |
There was a problem hiding this comment.
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.
| export const DEFAULT_PROFILE: Profile = { | ||
| id: "default", | ||
| name: "Default", | ||
| icon: "", |
There was a problem hiding this comment.
Same thing here, this icon isn't rendering.
| surfaceAlt: "#1f1f1f", | ||
| input: "#111111", | ||
| surfaceUser: "#202020", | ||
| canvas: "#21141e", |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
You should make sure to leave this as false by default. Remember, the core of t1code is the coding mode.
| const [tempChatMode, setTempChatMode] = useState(false); | ||
| const [sidebarSearchQuery, setSidebarSearchQuery] = useState(""); | ||
| const [chatProfiles, setChatProfiles] = useState<import("./profiles").Profile[]>([ | ||
| { id: "default", name: "Default", icon: "" }, |
There was a problem hiding this comment.
Again, use global emojis here, this icon isn't rendering on my machine.
| width: responsiveLayout.showSidebar ? responsiveLayout.sidebarWidth : TUI_SIDEBAR_WIDTH, | ||
| backgroundColor: sidebarBg, | ||
| border: ["right"], | ||
| border: isChatMode ? [] : ["right"], |
There was a problem hiding this comment.
Was leaving this blank for isChatMode intended?
|
|
||
| type TimeGroup = { label: string; threads: typeof allThreads }; | ||
| const groups: TimeGroup[] = [ | ||
| { label: "Today", threads: [] }, |
There was a problem hiding this comment.
This could be a nice feature to implement globally, not just for the chat mode.
| style={{ | ||
| position: "absolute", | ||
| top: 4, | ||
| right: 1, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Same thing for the keybind control too.
| backgroundColor: appSettings.theme === "light" ? PALETTE.controlActive : "transparent", | ||
| }} | ||
| > | ||
| <text content="" style={{ fg: appSettings.theme === "light" ? PALETTE.text : PALETTE.muted }} /> |
There was a problem hiding this comment.
This icon isn't rendering either-- fix.
| backgroundColor: appSettings.theme === "system" || !appSettings.theme ? PALETTE.controlActive : "transparent", | ||
| }} | ||
| > | ||
| <text content="" style={{ fg: appSettings.theme === "system" || !appSettings.theme ? PALETTE.text : PALETTE.muted }} /> |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR adds a Rust/Wasm demo app with GitHub Pages deployment, and introduces a Changest1code-demo Wasm Application and Pages Deployment
t1chat Chat Mode for TUI
README Documentation Updates
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
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
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
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. Comment |
There was a problem hiding this comment.
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 winRange-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 memoizedallThreads(defined at ~line 3261) and is unfiltered by the active profile/search query. It's built at line 7933-7939, thenprofileThreads/filteredThreadsare derived from it for what's actually rendered — buthandleThreadClick(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 (seerangeSelectThread).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
allThreadsto avoid shadowing the outer component-scopeallThreads.🤖 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 liftDefault 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, andDEFAULT_LIGHT_PALETTEwholesale — these are the palettes returned forthemeId === "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 byisChatMode.#!/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 winPreviously 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
Tabfocus-order arrays formainView !== "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 winPotential 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 valueDead
..areastruct-update incontrolsRect (Clippy).All four fields (
x,y,width,height) are already specified, so..areahas 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_clickis unreachable dead code.
FOOTER_HEIGHTis0, solayout.footeralways has zero height;contains()can never return true for a zero-height rect, meaninghandle_mouse's call tohandle_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 inhandle_mouse, anddraw_footer's early-return guard if the footer row is no longer part of the design, or restoreFOOTER_HEIGHTif 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 valueCollapse redundant
ifinto 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 winRow-based click hit-testing uses magic numbers duplicated from render order.
handle_settings_view_click'srow.saturating_sub(area.y)matches (5, 6, 10, 13, 17) andthread_index_for_sidebar_row's absolute rows (9-12) only work because they happen to mirror the exact line order indraw_settings_view'slinesvec and theLayout::verticalheights indraw_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 tradeoffOverlay icon positions are hardcoded pixel values with no shared source of truth against the Rust layout.
Values like
left: 321px,bottom: 37px, and the varioustop/leftoffsets for.search-icon,.composer-toolbar-dom,.chat-settings-dom,.model-provider-icons, etc. must stay pixel-perfect in sync withSIDEBAR_WIDTH,COMPOSER_HEIGHT, and the row layout insrc/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 winExternal 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/crossoriginattributes, andhttps://www.nerdfonts.com/assets/css/webfont.csspulls 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 addintegrity/crossoriginattributes 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 winConsider 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 inuseMemo, unlike the analogousallThreads/threadsByProjectcomputations at the top of the component). SinceAppre-renders frequently (composer edits, animation ticks, etc.), wrapping this inuseMemokeyed 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
⛔ Files ignored due to path filters (3)
apps/t1code-demo/Cargo.lockis excluded by!**/*.lockassets/repo/t1chat-preview.pngis excluded by!**/*.pngbun.lockis excluded by!**/*.lock
📒 Files selected for processing (13)
.github/workflows/pages.yml.gitignoreREADME.mdapps/t1code-demo/Cargo.tomlapps/t1code-demo/index.htmlapps/t1code-demo/src/main.rsapps/tui/bin/t1chat.jsapps/tui/package.jsonapps/tui/src/profiles.tsapps/tui/src/responsiveLayout.tsapps/tui/src/theme.tsapps/tui/src/ui.tsxscripts/dev-tui.ts
| permissions: | ||
| contents: read | ||
| id-token: write | ||
| pages: write |
There was a problem hiding this comment.
🔒 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
| - name: Check out repository | ||
| uses: actions/checkout@v5 |
There was a problem hiding this comment.
🔒 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.
| - 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
| - name: Build demo | ||
| working-directory: apps/t1code-demo | ||
| run: trunk build --release --public-url "/${{ github.event.repository.name }}/" |
There was a problem hiding this comment.
🔒 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.
| - 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
| 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> | ||
| ); | ||
| } | ||
|
|
There was a problem hiding this comment.
🎯 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.
| 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.
|
|
||
| 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); |
There was a problem hiding this comment.
🗄️ 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.
| {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} |
There was a problem hiding this comment.
🎯 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]}")
PYRepository: 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.tsxRepository: 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.tsxRepository: 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.
| {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 ? ( |
There was a problem hiding this comment.
🎯 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.
| {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.
| <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> |
There was a problem hiding this comment.
🎯 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.
| git clone https://github.com/ahzs645/t1chat.git | ||
| cd t1code | ||
| bun install | ||
| bun dev:tui | ||
| ``` |
There was a problem hiding this comment.
📐 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.
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
Summary by CodeRabbit
t1code-demoapp.apps/*/target) in version control.