Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,30 @@ h1, h2, h3, h4, h5, h6 {
.animate-name-shimmer {
animation: qw-name-shimmer 1.6s ease-in-out infinite;
}

/* Korean help text should wrap by word, not by individual syllable.
Keep long URLs/tokens from overflowing narrow tooltips and modals. */
.ko-help {
word-break: keep-all;
overflow-wrap: normal;
line-break: strict;
text-wrap: pretty;
}

.ko-help p,
.ko-help li,
.ko-help div,
.ko-help span,
.ko-help b,
.ko-help strong {
word-break: inherit;
overflow-wrap: inherit;
line-break: inherit;
}

.ko-help code,
.ko-help pre,
.ko-help .break-anywhere {
word-break: normal;
overflow-wrap: anywhere;
}
15 changes: 9 additions & 6 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "./globals.css";
import Sidebar from "@/components/Sidebar";
import TopHeader from "@/components/TopHeader";
import GlobalNotificationListener from "@/components/GlobalNotificationListener";
import { LocaleProvider } from "@/components/LocaleProvider";

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
Expand All @@ -23,12 +24,14 @@ export default function RootLayout({
return (
<html lang="en" className={`${geistMono.variable} h-full`}>
<body className="h-full flex flex-col">
<GlobalNotificationListener />
<TopHeader />
<div className="flex flex-1 min-h-0">
<Sidebar />
<main className="flex-1 min-w-0 overflow-auto">{children}</main>
</div>
<LocaleProvider>
<GlobalNotificationListener />
<TopHeader />
<div className="flex flex-1 min-h-0">
<Sidebar />
<main className="flex-1 min-w-0 overflow-auto">{children}</main>
</div>
</LocaleProvider>
</body>
</html>
);
Expand Down
93 changes: 78 additions & 15 deletions src/components/AgentModelsWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,66 @@

import { useCallback, useEffect, useState } from "react";
import InfoTooltip from "./InfoTooltip";
import { useLocale } from "@/components/LocaleProvider";

const COPY = {
en: {
title: "Agent Models",
loading: "Loading…",
noAgents: "No agents configured.",
restartRequired: "restart required",
restartRequiredTooltip: "Config changed — running session is still on the old model/effort. Click Restart to apply.",
default: "(default)",
custom: "(custom)",
restart: "Restart",
restartTooltip: "Restart this agent to pick up the new model / reasoning setting",
help: (
<>
Codex reasoning effort defaults to <code className="text-text">medium</code> for new projects. Blank model falls back to the CLI default. Click Restart to apply changes to a live session.
</>
),
summary: (id: string, backend: string) => (
<>
<span className="text-text">{id}</span>
<span>: {backend}</span>
</>
),
configure: "Configure →",
tooltip: (
<>
<b>Agent Models</b> — configure which LLM model and reasoning effort each agent uses. Changes require an agent restart to take effect.
</>
),
},
ko: {
title: "에이전트 모델",
loading: "로딩 중…",
noAgents: "설정된 에이전트가 없습니다.",
restartRequired: "재시작 필요",
restartRequiredTooltip: "설정이 변경되었습니다. 실행 중인 세션에는 이전 설정이 적용되어 있습니다. 재시작을 클릭하여 적용하세요.",
default: "(기본값)",
custom: "(사용자 정의)",
restart: "재시작",
restartTooltip: "에이전트를 재시작하여 새로운 모델/추론 설정을 적용합니다",
help: (
<>
Codex 추론 수준은 새 프로젝트의 경우 <code className="text-text">medium</code>으로 기본 설정됩니다. 모델을 비워두면 CLI 기본값이 사용됩니다. 변경 사항을 적용하려면 재시작을 클릭하세요.
</>
),
summary: (id: string, backend: string) => (
<>
<span className="text-text">{id}</span>
<span>: {backend}</span>
</>
),
configure: "설정 →",
tooltip: (
<>
<b>에이전트 모델</b> - 각 에이전트가 어떤 LLM 모델과 추론 수준을 사용할지 설정합니다. 변경 사항은 에이전트를 재시작해야 적용됩니다.
</>
),
},
} as const;

interface AgentRow {
agent_id: string;
Expand Down Expand Up @@ -68,6 +128,8 @@ function optionsForBackend(backend: string) {
// state. Mounted only when the modal is open so we don't run the
// fetch until the operator actually wants to configure something.
function AgentModelsModal({ projectId, onClose }: { projectId: string; onClose: () => void }) {
const { locale } = useLocale();
const t = COPY[locale];
const [rows, setRows] = useState<AgentRow[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState<string | null>(null);
Expand Down Expand Up @@ -172,14 +234,14 @@ function AgentModelsModal({ projectId, onClose }: { projectId: string; onClose:
</button>

<div className="flex items-center justify-between mb-3">
<h2 id="agent-models-title" className="text-base font-semibold text-white">Agent Models</h2>
<h2 id="agent-models-title" className="text-base font-semibold text-white">{t.title}</h2>
{error && <span className="text-[10px] text-error max-w-[60%] truncate ml-2" title={error}>err: {error}</span>}
</div>

<div className="flex flex-col gap-1.5">
{!rows && <div className="text-[11px] text-text-muted">Loading…</div>}
{!rows && <div className="text-[11px] text-text-muted">{t.loading}</div>}
{rows && rows.length === 0 && (
<div className="text-[11px] text-text-muted">No agents configured.</div>
<div className="text-[11px] text-text-muted">{t.noAgents}</div>
)}
{rows && rows.map((row) => (
<div key={row.agent_id} className="flex items-center gap-1.5 flex-wrap">
Expand All @@ -188,9 +250,9 @@ function AgentModelsModal({ projectId, onClose }: { projectId: string; onClose:
{needsRestart.has(row.agent_id) && (
<span
className="text-[9px] text-[#ffcc00] border border-[#ffcc00]/40 px-1 py-[1px] shrink-0"
title="Config changed — running session is still on the old model/effort. Click Restart to apply."
title={t.restartRequiredTooltip}
>
restart required
{t.restartRequired}
</span>
)}
{/* #343: backend-specific model dropdown. Empty value
Expand All @@ -210,7 +272,7 @@ function AgentModelsModal({ projectId, onClose }: { projectId: string; onClose:
(e.g. operator hand-edited config.json), keep
it selectable so their override doesn't vanish. */}
{row.model && !optionsForBackend(row.backend).some((o) => o.value === row.model) && (
<option value={row.model} className="bg-bg-surface">{row.model} (custom)</option>
<option value={row.model} className="bg-bg-surface">{row.model} {t.custom}</option>
)}
</select>
{row.reasoning_supported ? (
Expand All @@ -220,7 +282,7 @@ function AgentModelsModal({ projectId, onClose }: { projectId: string; onClose:
onChange={(e) => update(row.agent_id, { reasoning_effort: e.target.value })}
className="bg-transparent border border-border px-1 py-0.5 text-[11px] text-text outline-none focus:border-accent cursor-pointer disabled:opacity-50"
>
<option value="" className="bg-bg-surface">(default)</option>
<option value="" className="bg-bg-surface">{t.default}</option>
{REASONING_LEVELS.map((lvl) => (
<option key={lvl} value={lvl} className="bg-bg-surface">{lvl}</option>
))}
Expand All @@ -232,15 +294,15 @@ function AgentModelsModal({ projectId, onClose }: { projectId: string; onClose:
type="button"
onClick={() => restart(row.agent_id)}
disabled={busy === row.agent_id}
title="Restart this agent to pick up the new model / reasoning setting"
title={t.restartTooltip}
className="shrink-0 px-1.5 py-0.5 text-[10px] text-text-muted border border-border hover:text-accent hover:border-accent/40 disabled:opacity-50 transition-colors"
>
{busy === row.agent_id ? "…" : "Restart"}
{busy === row.agent_id ? "…" : t.restart}
</button>
</div>
))}
<p className="mt-2 text-[10px] text-text-muted leading-snug">
Codex reasoning effort defaults to <code className="text-text">medium</code> for new projects. Blank model falls back to the CLI default. Click Restart to apply changes to a live session.
{t.help}
</p>
</div>
</div>
Expand All @@ -257,6 +319,8 @@ function AgentModelsModal({ projectId, onClose }: { projectId: string; onClose:
// component init; we don't poll, since backends rarely change
// outside the modal flow.
export default function AgentModelsButton({ projectId }: AgentModelsWidgetProps) {
const { locale } = useLocale();
const t = COPY[locale];
const [open, setOpen] = useState(false);
const [summary, setSummary] = useState<{ id: string; backend: string }[] | null>(null);

Expand All @@ -278,26 +342,25 @@ export default function AgentModelsButton({ projectId }: AgentModelsWidgetProps)
<div className="flex flex-col border border-border">
<div className="flex items-center justify-between h-7 px-3 shrink-0 border-b border-border">
<div className="flex items-center gap-1.5">
<span className="text-[11px] text-text-muted uppercase tracking-wider">Agent Models</span>
<span className="text-[11px] text-text-muted uppercase tracking-wider">{t.title}</span>
<InfoTooltip>
<b>Agent Models</b> — configure which LLM model and reasoning effort each agent uses. Changes require an agent restart to take effect.
{t.tooltip}
</InfoTooltip>
</div>
<button
type="button"
onClick={() => setOpen(true)}
className="px-2 py-0.5 text-[10px] text-text-muted border border-border hover:text-accent hover:border-accent/40 transition-colors"
>
Configure →
{t.configure}
</button>
</div>
{summary && summary.length > 0 && (
<div className="px-3 py-1 text-[10px] text-text-muted truncate" title={summary.map((s) => `${s.id}: ${s.backend}`).join(" · ")}>
{summary.map((s, i) => (
<span key={s.id}>
{i > 0 && <span className="text-text-muted/60"> · </span>}
<span className="text-text">{s.id}</span>
<span>: {s.backend}</span>
{t.summary(s.id, s.backend)}
</span>
))}
</div>
Expand Down
35 changes: 28 additions & 7 deletions src/components/AgentTerminalsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@

import { useState } from "react";
import TerminalGrid from "./TerminalGrid";
import { useLocale } from "@/components/LocaleProvider";

const COPY = {
en: {
title: "Agent Terminals",
aboutLabel: "About agent terminals",
tooltip: (
<>
These show what each agent is doing in their CLI session. <b>Do not type here directly</b> — use the AgentChattr chat above instead. Agents won&apos;t see messages typed in their terminals.
</>
),
},
ko: {
title: "에이전트 터미널",
aboutLabel: "에이전트 터미널 설명",
tooltip: (
<>
각 에이전트가 CLI 세션에서 무엇을 하고 있는지 보여주는 읽기 전용 터미널입니다. <b>여기에 직접 입력하지 마세요.</b> 위의 AgentChattr 채팅을 사용해야 에이전트가 메시지를 볼 수 있습니다.
</>
),
},
} as const;

// #208: the top-right quadrant must show all four agents
// (Head, RE1, RE2, Dev) as a 2x2 grid. TerminalGrid's
Expand Down Expand Up @@ -41,13 +63,15 @@ interface AgentTerminalsGridProps {
* the terminals and their messages are lost to the other agents.
*/
export default function AgentTerminalsGrid({ projectId, agentStates, onStatusChange }: AgentTerminalsGridProps) {
const { locale } = useLocale();
const t = COPY[locale];
const [tipOpen, setTipOpen] = useState(false);

return (
<div className="flex flex-col h-full min-h-0">
<div className="flex items-center justify-between h-7 px-3 shrink-0 border-b border-border">
<div className="flex items-center gap-1.5">
<span className="text-[11px] text-text-muted uppercase tracking-wider">Agent Terminals</span>
<span className="text-[11px] text-text-muted uppercase tracking-wider">{t.title}</span>
<div
// #399 / quadwork#264: inline-flex+items-center so the
// (?) button vertically centers with the title text. The
Expand All @@ -61,18 +85,15 @@ export default function AgentTerminalsGrid({ projectId, agentStates, onStatusCha
>
<button
type="button"
aria-label="About agent terminals"
aria-label={t.aboutLabel}
className="w-3.5 h-3.5 rounded-full border border-border text-[9px] leading-none text-text-muted hover:text-accent hover:border-accent inline-flex items-center justify-center"
>?</button>
{tipOpen && (
<div
role="tooltip"
className="absolute top-5 left-0 z-20 w-72 p-2 text-[11px] leading-snug text-text bg-bg-surface border border-border shadow-lg"
className="ko-help absolute top-5 left-0 z-20 w-72 max-w-[min(18rem,calc(100vw-2rem))] p-2 text-[11px] leading-snug text-text bg-bg-surface border border-border shadow-lg"
>
These show what each agent is doing in their CLI session.{" "}
<b>Do not type here directly</b> — use the AgentChattr chat
above instead. Agents won&apos;t see messages typed in their
terminals.
{t.tooltip}
</div>
)}
</div>
Expand Down
Loading