Compare commits

...

2 Commits

Author SHA1 Message Date
3641190a77 feat: ekip paneli ve yonlendirme davranisini iyilestir 2026-03-17 00:02:17 +03:00
89e715cf1c docs: proje icin README hazirla 2026-03-17 00:02:02 +03:00
8 changed files with 259 additions and 72 deletions

174
README.md
View File

@@ -0,0 +1,174 @@
# Retro Claude Team Console 🖥️✨
90'lar retro/pixel estetiğiyle hazırlanmış, Claude CLI oturumunu web arayüzünden yöneten deneysel bir ekip konsolu.
Amaç: tek bir web uygulaması üzerinden Claude oturumu başlatmak, team mode bootstrap prompt'unu göndermek, canlı cevap akışını izlemek ve ekip üyelerinin yanıtlarını rol bazlı kartlarda görmek.
## Özellikler 🚀
- Canlı Claude oturumu başlatma
- `Activate Team` ile bootstrap prompt gönderme
- Retro/pixel web konsol arayüzü
- Sol panelde canlı ana akış
- Sağ panelde ekip üyelerine göre ayrılmış kartlar
- Hedef kişiye göre yönlendirilmiş prompt gönderimi
- `Mazlum:`, `Simsar:`, `Aybuke:` gibi etiketli cevap formatı
- `tmux` tabanlı PTY oturumu yönetimi
## Ekip Yapısı 👥
- Mazlum: Team Lead
- Berkecan: Frontend Developer
- Simsar: Backend Developer
- Aybuke: UI/UX Designer
- Ive: iOS Developer
- Irgatov: Trainee
## Teknoloji Yığını 🧰
- Node.js
- Express
- Socket.IO
- React
- Vite
- `tmux`
## Gereksinimler 📦
- Node.js
- npm
- `tmux`
- makinede erişilebilir bir `claude` binary
Kontrol etmek için:
```bash
node -v
npm -v
tmux -V
claude --version
```
## Ortam Değişkenleri 🔐
Örnek `.env`:
```env
API_KEY_PRO="..."
API_KEY_LITE="..."
ACTIVE_KEY=pro
ANTHROPIC_BASE_URL="https://api.z.ai/api/anthropic"
ANTHROPIC_MODEL="glm-5"
```
İsteğe bağlı değişkenler:
```env
PORT=3001
CLAUDE_BIN=claude
CLAUDE_SHELL=/bin/zsh
CLAUDE_WORKSPACE_DIR=/path/to/workspace
CLAUDE_ARGS=--dangerously-skip-permissions
WATCH_LOG_LIMIT=400
CHAT_CHUNK_LIMIT=2000
LOG_TO_CONSOLE=true
```
## Kurulum 🛠️
```bash
npm install
```
## Geliştirme Modu ▶️
```bash
npm run dev
```
Bu komut:
- backend'i `http://localhost:3001`
- frontend'i `http://localhost:3000`
adresinde çalıştırır.
## Production Build 📦
```bash
npm run build
npm run start
```
## Kullanım Akışı 🎮
1. Uygulamayı
2. `Start Session` ile Claude oturumunu başlat
3. `Activate Team` ile ekip bootstrap prompt'unu gönder
4. Bir ekip üyesine ya da tüm takıma prompt yaz
5. Sol panelde ana akışı izle
6. Sağ panelde rol bazlı kartları takip et
## Prompt Davranışı 🧠
Sistem şu mantıkla çalışır:
- Kullanıcı mesajında bir ekip üyesinin adı geçerse prompt o kişiye yönlendirilir
- Kısa takip mesajları mümkünse son hedef kişiye bağlanır
- Yanıtların `Mazlum:` / `Simsar:` gibi isim etiketiyle başlaması zorlanır
- Sağ paneldeki kartlar bu etiketlere göre doldurulur
Örnek:
```text
Mazlum nasılsın?
```
Beklenen yanıt:
```text
Mazlum: İyiyim, teşekkür ederim!
```
## Proje Yapısı 🗂️
```text
server/
bootstrapPrompt.js
config.js
index.js
logService.js
ptyService.js
sessionManager.js
socketHandlers.js
teamConfig.js
web/
index.html
vite.config.js
src/
App.jsx
components/
hooks/
lib/
styles/
```
## Bilinen Notlar ⚠️
- Claude bazen gelen yönlendirme metnini literal yorumlayabilir; routing mantığı hâlâ iyileştirilmeye açık.
- Kart parser'ı etiketli cevap formatına dayanır; format bozulursa bazı mesajlar yanlış karta düşebilir veya hiç görünmeyebilir.
- `Auth conflict` uyarısı Claude tarafındaki oturum durumuna bağlı olarak görülebilir.
- Bu proje şu anda deneysel bir konsol prototipi olarak düşünülmelidir.
## Yakın Yol Haritası 🛣️
- Kart parser'ını daha akıllı hale getirmek
- Kullanıcı mesajlarını da role bazlı akışta göstermek
- Watch/debug görünümünü opsiyonel olarak geri eklemek
- Session geçmişi ve kalıcı log desteği eklemek
- Ekip içi konuşmaları daha güvenilir ayrıştırmak
## Lisans 📄
Bu repo için henüz ayrı bir lisans dosyası tanımlanmadı.

View File

@@ -26,27 +26,13 @@ function buildRoutedPrompt(prompt, lastDirectedMember = null) {
if (!targetMember) { if (!targetMember) {
return { return {
targetMember: null, targetMember: null,
routedPrompt: [ routedPrompt: `Not: Bu genel mesajdir. Once Mazlum cevap versin ve konusan herkes ad etiketi kullansin. Kullanici mesaji: ${prompt}`
"[YONLENDIRME NOTU - CEVAPTA TEKRAR ETME]",
"Bu mesaj genel bir gorev veya genel konusmadir.",
"Cevabi once Mazlum baslatsin.",
"Konusan herkes ad etiketi kullansin.",
"[KULLANICI MESAJI]",
prompt
].join("\n")
}; };
} }
return { return {
targetMember, targetMember,
routedPrompt: [ routedPrompt: `Not: Bu mesaj ${targetMember.name} icindir. Yalnizca ${targetMember.name} cevap versin ve cevap \`${targetMember.name}:\` ile baslasin. Kullanici mesaji: ${prompt}`
"[YONLENDIRME NOTU - CEVAPTA TEKRAR ETME]",
`Bu mesaj ${targetMember.name} icindir.`,
`Yalnizca ${targetMember.name} cevap versin.`,
`Cevap \`${targetMember.name}:\` ile baslasin.`,
"[KULLANICI MESAJI]",
prompt
].join("\n")
}; };
} }

View File

@@ -1,6 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import ShellFrame from "./components/ShellFrame.jsx"; import ShellFrame from "./components/ShellFrame.jsx";
import StatusStrip from "./components/StatusStrip.jsx";
import SessionToolbar from "./components/SessionToolbar.jsx"; import SessionToolbar from "./components/SessionToolbar.jsx";
import ChatStream from "./components/ChatStream.jsx"; import ChatStream from "./components/ChatStream.jsx";
import PromptComposer from "./components/PromptComposer.jsx"; import PromptComposer from "./components/PromptComposer.jsx";
@@ -32,35 +31,40 @@ export default function App() {
<h1>Retro Claude Team Console</h1> <h1>Retro Claude Team Console</h1>
</div> </div>
<div className="app-shell__meta"> <div className="app-shell__meta">
<span>LIVE STREAM</span> <span>LINK: {connected ? "ONLINE" : "OFFLINE"}</span>
<span>TEAM COMMS</span> <span>SESSION: {String(session.status || "idle").toUpperCase()}</span>
<span>PIXEL MODE</span> <span>TEAM: {session.teamActivated ? "ACTIVE" : "STANDBY"}</span>
<span>MODEL: {session.runtime?.anthropicModel || "N/A"}</span>
<span>KEY: {String(session.runtime?.activeKey || "pro").toUpperCase()}</span>
</div> </div>
</div> </div>
<ShellFrame> <ShellFrame>
<StatusStrip connected={connected} session={session} />
<SessionToolbar
session={session}
busy={busy}
onStart={() => runAction(startSession)}
onActivate={() => runAction(activateTeam)}
onStop={() => runAction(stopSession)}
/>
{error ? <div className="error-banner">{error}</div> : null} {error ? <div className="error-banner">{error}</div> : null}
<div className="console-grid"> <div className="console-grid">
<div className="console-grid__side">
<TeamBoard chat={chat} />
</div>
<div className="console-grid__main"> <div className="console-grid__main">
<ChatStream chat={chat} session={session} /> <ChatStream
chat={chat}
session={session}
headerExtra={
<SessionToolbar
session={session}
busy={busy}
onStart={() => runAction(startSession)}
onActivate={() => runAction(activateTeam)}
onStop={() => runAction(stopSession)}
/>
}
/>
<PromptComposer <PromptComposer
disabled={busy || session.status !== "running"} disabled={busy || session.status !== "running"}
onSubmit={(prompt) => runAction(() => sendPrompt(prompt))} onSubmit={(prompt) => runAction(() => sendPrompt(prompt))}
/> />
</div> </div>
<div className="console-grid__side">
<TeamBoard chat={chat} />
</div>
</div> </div>
</ShellFrame> </ShellFrame>
</main> </main>

View File

@@ -1,7 +1,7 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import PanelFrame from "./PanelFrame.jsx"; import PanelFrame from "./PanelFrame.jsx";
export default function ChatStream({ chat, session }) { export default function ChatStream({ chat, session, headerExtra = null }) {
const scrollerRef = useRef(null); const scrollerRef = useRef(null);
useEffect(() => { useEffect(() => {
@@ -16,7 +16,12 @@ export default function ChatStream({ chat, session }) {
const isEmpty = !chat.trim(); const isEmpty = !chat.trim();
return ( return (
<PanelFrame title="Claude Live Feed" eyebrow="PRIMARY STREAM" className="chat-panel"> <PanelFrame
title="Claude Live Feed"
eyebrow="PRIMARY STREAM"
className="chat-panel"
headerExtra={headerExtra}
>
<div className="chat-stream" ref={scrollerRef}> <div className="chat-stream" ref={scrollerRef}>
{isEmpty ? ( {isEmpty ? (
<div className="empty-state"> <div className="empty-state">

View File

@@ -1,4 +1,4 @@
export default function PanelFrame({ title, eyebrow, children, className = "" }) { export default function PanelFrame({ title, eyebrow, children, className = "", headerExtra = null }) {
return ( return (
<section className={`panel-frame ${className}`}> <section className={`panel-frame ${className}`}>
<div className="panel-frame__header"> <div className="panel-frame__header">
@@ -6,6 +6,7 @@ export default function PanelFrame({ title, eyebrow, children, className = "" })
<p className="panel-frame__eyebrow">{eyebrow}</p> <p className="panel-frame__eyebrow">{eyebrow}</p>
<h2 className="panel-frame__title">{title}</h2> <h2 className="panel-frame__title">{title}</h2>
</div> </div>
{headerExtra ? <div className="panel-frame__extra">{headerExtra}</div> : null}
</div> </div>
<div className="panel-frame__body">{children}</div> <div className="panel-frame__body">{children}</div>
</section> </section>

View File

@@ -5,7 +5,7 @@ export default function SessionToolbar({ session, busy, onStart, onActivate, onS
const isStarting = session.status === "starting"; const isStarting = session.status === "starting";
return ( return (
<div className="session-toolbar"> <div className="session-toolbar session-toolbar--inline">
<PixelButton tone="green" disabled={busy || isRunning || isStarting} onClick={onStart}> <PixelButton tone="green" disabled={busy || isRunning || isStarting} onClick={onStart}>
Start Session Start Session
</PixelButton> </PixelButton>

View File

@@ -81,6 +81,8 @@ function isContinuationLine(line) {
} }
return [ return [
/^[•\-]\s+/.test(trimmed),
/^[0-9]+\.\s+/.test(trimmed),
/^[A-Za-zÀ-ÿ0-9ÇĞİÖŞÜçğıöşü"'`(]/.test(trimmed), /^[A-Za-zÀ-ÿ0-9ÇĞİÖŞÜçğıöşü"'`(]/.test(trimmed),
/^[.!?…]/.test(trimmed), /^[.!?…]/.test(trimmed),
/^💪|^😊|^🚀|^☕|^🎨|^📱/.test(trimmed) /^💪|^😊|^🚀|^☕|^🎨|^📱/.test(trimmed)
@@ -115,7 +117,9 @@ export function parseTeamFeed(chat) {
if (speakerMatch) { if (speakerMatch) {
const member = memberMap.get(normalizeSpeaker(speakerMatch[1])); const member = memberMap.get(normalizeSpeaker(speakerMatch[1]));
if (!member) { if (!member) {
currentEntry = null; if (currentEntry && isContinuationLine(line)) {
currentEntry.text = currentEntry.text ? `${currentEntry.text}\n${line}` : line;
}
continue; continue;
} }

View File

@@ -7,8 +7,8 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: 24px; gap: 24px;
align-items: end; align-items: start;
margin-bottom: 24px; margin-bottom: 18px;
} }
.app-shell__eyebrow, .app-shell__eyebrow,
@@ -39,14 +39,15 @@
display: flex; display: flex;
gap: 10px; gap: 10px;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end;
} }
.app-shell__meta span { .app-shell__meta span {
padding: 8px 12px; padding: 6px 10px;
border: 2px solid var(--border-mid); border: 1px solid rgba(70, 121, 84, 0.45);
background: rgba(15, 22, 17, 0.8); background: rgba(15, 22, 17, 0.56);
color: var(--text-dim); color: var(--text-dim);
font-size: 0.75rem; font-size: 0.7rem;
} }
.shell-frame { .shell-frame {
@@ -80,14 +81,6 @@
z-index: 1; z-index: 1;
} }
.status-strip {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 10px;
margin-bottom: 14px;
}
.status-strip__cell,
.panel-frame, .panel-frame,
.prompt-composer, .prompt-composer,
.error-banner { .error-banner {
@@ -96,15 +89,6 @@
background: linear-gradient(180deg, rgba(26, 34, 29, 0.95) 0%, rgba(14, 19, 16, 0.95) 100%); background: linear-gradient(180deg, rgba(26, 34, 29, 0.95) 0%, rgba(14, 19, 16, 0.95) 100%);
} }
.status-strip__cell {
padding: 12px;
}
.status-strip__cell strong {
font-size: 0.95rem;
letter-spacing: 0.08em;
}
.is-green { .is-green {
color: var(--accent-green); color: var(--accent-green);
} }
@@ -125,7 +109,11 @@
display: flex; display: flex;
gap: 12px; gap: 12px;
flex-wrap: wrap; flex-wrap: wrap;
margin-bottom: 16px; }
.session-toolbar--inline {
justify-content: flex-end;
margin-bottom: 0;
} }
.pixel-button { .pixel-button {
@@ -191,6 +179,7 @@
display: grid; display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.9fr); grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.9fr);
gap: 16px; gap: 16px;
align-items: start;
} }
.console-grid__main, .console-grid__main,
@@ -201,14 +190,30 @@
min-width: 0; min-width: 0;
} }
.console-grid__side {
min-width: 0;
}
.console-grid__main {
min-width: 0;
}
.panel-frame__header { .panel-frame__header {
padding: 14px 16px 0; padding: 14px 16px 0;
display: flex;
justify-content: space-between;
gap: 14px;
align-items: start;
} }
.panel-frame__body { .panel-frame__body {
padding: 14px 16px 16px; padding: 14px 16px 16px;
} }
.panel-frame__extra {
flex-shrink: 0;
}
.chat-stream, .chat-stream,
.team-card__body { .team-card__body {
min-height: 420px; min-height: 420px;
@@ -281,13 +286,16 @@
.team-board { .team-board {
display: grid; display: grid;
gap: 12px; grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
align-items: start;
} }
.team-card { .team-card {
border: 2px solid var(--border-mid); border: 2px solid var(--border-mid);
background: linear-gradient(180deg, rgba(10, 16, 11, 0.92), rgba(8, 12, 9, 0.96)); background: linear-gradient(180deg, rgba(10, 16, 11, 0.92), rgba(8, 12, 9, 0.96));
box-shadow: inset 0 0 0 1px rgba(114, 255, 132, 0.08); box-shadow: inset 0 0 0 1px rgba(114, 255, 132, 0.08);
min-height: 348px;
} }
.team-card__header { .team-card__header {
@@ -342,8 +350,8 @@
} }
.team-card__body { .team-card__body {
min-height: 124px; min-height: 276px;
max-height: 190px; max-height: 276px;
} }
.team-message { .team-message {
@@ -373,11 +381,11 @@
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
.status-strip { .console-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: 1fr;
} }
.console-grid { .team-board {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@@ -393,12 +401,17 @@
align-items: stretch; align-items: stretch;
} }
.status-strip {
grid-template-columns: 1fr;
}
.shell-frame__screen { .shell-frame__screen {
min-height: auto; min-height: auto;
padding: 12px; padding: 12px;
} }
.panel-frame__header {
flex-direction: column;
align-items: stretch;
}
.session-toolbar--inline {
justify-content: stretch;
}
} }