From 0deec4190bfd84608bcafa7ff87c804a09c82604 Mon Sep 17 00:00:00 2001 From: katipally Date: Tue, 23 Jun 2026 11:09:25 -0700 Subject: [PATCH 1/7] bugs fixes v1 --- CHANGELOG.md | 36 +++++ docs/configuration.md | 24 +++ packages/core/src/config.ts | 51 +++++- packages/core/src/engine.ts | 15 +- packages/core/src/fleet.ts | 80 +++++---- packages/core/src/keybindings.ts | 5 +- packages/core/src/runner.ts | 34 +++- packages/core/src/sessions.ts | 28 +++- packages/core/test/config.test.ts | 36 +++++ packages/core/test/keybindings.test.ts | 5 +- packages/providers/src/paths.ts | 8 + packages/shared/src/theme.ts | 104 ++++++------ packages/shared/test/theme.test.ts | 2 +- packages/tui/src/App.tsx | 12 ++ packages/tui/src/components/ComputerModal.tsx | 12 ++ packages/tui/src/components/ConsoleView.tsx | 3 +- .../tui/src/components/ContextFilesModal.tsx | 152 ++++++++++++++++++ packages/tui/src/components/ContextPanel.tsx | 36 +++-- packages/tui/src/components/EffortSlider.tsx | 69 ++++---- packages/tui/src/components/StatusStrip.tsx | 8 +- packages/tui/src/index.tsx | 6 + packages/tui/src/store.tsx | 48 +++++- packages/tui/src/util/term.ts | 19 +++ packages/tui/test/keymap-pause.test.tsx | 56 +++++++ 24 files changed, 703 insertions(+), 146 deletions(-) create mode 100644 packages/core/test/config.test.ts create mode 100644 packages/tui/src/components/ContextFilesModal.tsx create mode 100644 packages/tui/test/keymap-pause.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index b33e899..71e970b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,42 @@ All notable changes to this project are documented here. The format is based on > for the next release. Each tag push also gets a fresh entry generated by the > `softprops/action-gh-release` `generate_release_notes: true` step. +## [Unreleased] + +### Added + +- **Pin context files**: a permanent, clickable context-files chip in the side + panel opens a new modal to view auto-loaded context (FRIDAY.md/AGENTS.md) and + pin project files into context; pins persist for the session. +- **Layered config** like Claude Code: `~/.friday/config.json` (user) → + `.friday/settings.json` (project, committed) → `.friday/settings.local.json` + (project-local, gitignored), deep-merged. See `docs/configuration.md`. +- **Computer-use permissions**: macOS buttons in the computer-use modal that open + the Accessibility and Screen Recording panes directly so you can click-grant. +- **Kitty keyboard protocol** enabled where supported, so chords legacy terminals + can't encode (Shift+Enter, Cmd+Enter) work in kitty/WezTerm/Ghostty/modern iTerm2. + +### Changed + +- **Pause keybinding** moved off `Shift+Esc` (terminals can't encode it — it + arrived as plain Esc) onto **Ctrl+P** everywhere, plus **Cmd+Enter** on + kitty-protocol terminals. Newline stays Shift+Enter / Option+Enter. +- **Side panel**: removed the redundant "context" title; the close control is + right-aligned with its `ctrl+b` shortcut shown clearly. +- **Plans/Todos/Changes sections**: reversed the washed-out hierarchy — section + titles are now bright and bold, counts/meta stay faint; sections are spaced apart. +- **Light theme** rebuilt as a warm-paper palette (Claude-Code-style) instead of + harsh pure white, keeping the amber brand; high-contrast pushed further from dark. +- **Effort slider** redrawn as a colored cool→warm intensity bar-ramp. +- **Icon consistency**: MCP (`⧉`), skills (`◆`), tool (`⚙`) and context (`📎`) now + use distinct glyphs from a central table instead of all sharing `⚡`/`✓`. + +### Fixed + +- **Dashboard terminal windows** failing silently: the launcher now resolves the + friday executable robustly (absolute path / PATH fallback) and surfaces the real + error in a toast instead of a generic "no backend" message. + ## [2.0.9] - 2026-06-23 ### Added diff --git a/docs/configuration.md b/docs/configuration.md index 71be29a..57d4968 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -48,6 +48,30 @@ list. Most of these can also be set from inside the TUI: `/model`, `/effort`, `/theme`, `/budget`, `/mcp`. Changes made there are written back to `config.json`. +## Settings precedence (user → project → local) + +Like Claude Code, config is layered. Friday reads three files and deep-merges +them, with later layers winning (nested objects merge key-by-key; arrays and +scalars are replaced): + +1. `~/.friday/config.json` — **user** settings, apply everywhere. +2. `.friday/settings.json` — **project** settings, committed to the repo so the + whole team shares them (model, mcp, hooks, bash policy, theme…). +3. `.friday/settings.local.json` — **project-local** overrides, **gitignored**; + your personal tweaks for this checkout. + +All three use the same shape as `config.json` above. TUI changes are written to +the user file by default. Recommended `.gitignore` for a project that commits +its settings: + +```gitignore +.friday/* +!.friday/settings.json +``` + +That commits `.friday/settings.json` while keeping `settings.local.json` (and +everything else under `.friday/`) out of version control. + ## Bash allow and deny The `bash` block gates shell commands before they run. `deny` blocks a command diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 6f3f29c..0e5a9a9 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -1,6 +1,7 @@ import fs from "node:fs" +import path from "node:path" import type { McpServerConfig } from "@friday/mcp" -import { configPath, fridayDir } from "@friday/providers" +import { configPath, fridayDir, projectConfigPath, projectLocalConfigPath } from "@friday/providers" import type { Effort, ModeId } from "@friday/shared" import type { HooksConfig } from "./hooks.ts" import type { MicConfig } from "./mic.ts" @@ -45,21 +46,55 @@ export interface FridayConfig { autoCompactThreshold?: number } -export function loadConfig(): FridayConfig { +/** Read & parse one JSON config file; missing/invalid files contribute nothing. */ +function readLayer(file: string): Partial { try { - return JSON.parse(fs.readFileSync(configPath(), "utf8")) + const parsed = JSON.parse(fs.readFileSync(file, "utf8")) + return parsed && typeof parsed === "object" ? parsed : {} } catch { return {} } } -export function saveConfig(patch: Partial): FridayConfig { - const next = { ...loadConfig(), ...patch } +/** Deep-merge plain objects (later wins); arrays and scalars are replaced, not concatenated. */ +function deepMerge(base: T, over: Partial): T { + const out: any = { ...base } + for (const [k, v] of Object.entries(over)) { + if (v === undefined) continue + const b = (out as any)[k] + out[k] = + b && v && typeof b === "object" && typeof v === "object" && !Array.isArray(b) && !Array.isArray(v) + ? deepMerge(b, v) + : v + } + return out +} + +/** + * Resolved config, layered like Claude Code: user (~/.friday/config.json) → project + * (.friday/settings.json, committed) → project-local (.friday/settings.local.json, gitignored). + * Later layers win; nested objects deep-merge so a project can override one key without dropping the + * rest. `cwd` defaults to the process cwd (the project root friday runs in). + */ +export function loadConfig(cwd: string = process.cwd()): FridayConfig { + let cfg = readLayer(configPath()) as FridayConfig + cfg = deepMerge(cfg, readLayer(projectConfigPath(cwd))) + cfg = deepMerge(cfg, readLayer(projectLocalConfigPath(cwd))) + return cfg +} + +/** + * Persist a patch. Writes to the USER config by default (the resolved view still layers project files + * on top at read time). Pass scope "project"/"local" to write the committed/gitignored project file. + */ +export function saveConfig(patch: Partial, scope: "user" | "project" | "local" = "user"): FridayConfig { + const file = scope === "project" ? projectConfigPath() : scope === "local" ? projectLocalConfigPath() : configPath() + const next = { ...readLayer(file), ...patch } try { - fs.mkdirSync(fridayDir(), { recursive: true }) - fs.writeFileSync(configPath(), JSON.stringify(next, null, 2)) + fs.mkdirSync(scope === "user" ? fridayDir() : path.dirname(file), { recursive: true }) + fs.writeFileSync(file, JSON.stringify(next, null, 2)) } catch { /* ignore */ } - return next + return loadConfig() } diff --git a/packages/core/src/engine.ts b/packages/core/src/engine.ts index b841832..5716dcb 100644 --- a/packages/core/src/engine.ts +++ b/packages/core/src/engine.ts @@ -434,11 +434,11 @@ export class Engine { return id ? this.teamPayload(id) : null } /** Pop a single agent out into a real terminal window (tmux pane / OS terminal). */ - popoutAgent(sessionId: string): { ok: boolean; backend: string; opened: number } { + popoutAgent(sessionId: string): { ok: boolean; backend: string; opened: number; error?: string } { return openFleetWindows([sessionId]) } /** Open a NEW interactive friday window: fresh chat (no args) or resume `-s `, in `cwd`. */ - openInteractive(args: string[] = [], cwd?: string): { ok: boolean; backend: string; opened: number } { + openInteractive(args: string[] = [], cwd?: string): { ok: boolean; backend: string; opened: number; error?: string } { return openInteractiveWindow(args, cwd ?? this.currentCwd()) } /** Force-activate deferred tools (by name prefix) for the focused session so the model can use them @@ -507,7 +507,7 @@ export class Engine { return uninstallComputerUse() } /** Open a viewer window per running task (tmux pane / OS terminal); returns the chosen backend. */ - openFleet(): { ok: boolean; backend: string; opened: number } { + openFleet(): { ok: boolean; backend: string; opened: number; error?: string } { const ids = this.taskList() .filter((t) => t.status === "running") .map((t) => t.id) @@ -711,9 +711,16 @@ export class Engine { currentRoots(): string[] { return this.focused().currentRoots() } - contextInfo(): { files: string[] } { + contextInfo(): { files: string[]; pinned: string[] } { return this.focused().contextInfo() } + /** Pin/unpin a file (relative to the focused session's primary root) into context for the session. */ + pinContextFile(rel: string): void { + this.focused().pinFile(rel) + } + unpinContextFile(rel: string): void { + this.focused().unpinFile(rel) + } listSkills(): SkillInfo[] { return this.focused().listSkills() } diff --git a/packages/core/src/fleet.ts b/packages/core/src/fleet.ts index f0daef0..0732329 100644 --- a/packages/core/src/fleet.ts +++ b/packages/core/src/fleet.ts @@ -9,17 +9,28 @@ * failed or opened blank behind the editor); the common emulators on Linux. Unknown env degrades to * "none" so the caller can fall back to the in-TUI view rather than guessing. * + * Failures are never swallowed: `run()` captures stderr and every result carries an `error` string so + * the dashboard can tell the user WHY a window didn't open instead of a generic "no backend". + * * ponytail: covers tmux + macOS (open .command) + common Linux emulators; add more as users hit them. */ import fs from "node:fs" import os from "node:os" import path from "node:path" -/** Reconstruct how to launch friday itself. Dev: `bun