Compare commits
2 Commits
b470f9e6cd
...
3641190a77
| Author | SHA1 | Date | |
|---|---|---|---|
| 3641190a77 | |||
| 89e715cf1c |
174
README.md
174
README.md
@@ -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ı aç
|
||||
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ı.
|
||||
|
||||
@@ -26,27 +26,13 @@ function buildRoutedPrompt(prompt, lastDirectedMember = null) {
|
||||
if (!targetMember) {
|
||||
return {
|
||||
targetMember: null,
|
||||
routedPrompt: [
|
||||
"[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")
|
||||
routedPrompt: `Not: Bu genel mesajdir. Once Mazlum cevap versin ve konusan herkes ad etiketi kullansin. Kullanici mesaji: ${prompt}`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
targetMember,
|
||||
routedPrompt: [
|
||||
"[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")
|
||||
routedPrompt: `Not: Bu mesaj ${targetMember.name} icindir. Yalnizca ${targetMember.name} cevap versin ve cevap \`${targetMember.name}:\` ile baslasin. Kullanici mesaji: ${prompt}`
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import ShellFrame from "./components/ShellFrame.jsx";
|
||||
import StatusStrip from "./components/StatusStrip.jsx";
|
||||
import SessionToolbar from "./components/SessionToolbar.jsx";
|
||||
import ChatStream from "./components/ChatStream.jsx";
|
||||
import PromptComposer from "./components/PromptComposer.jsx";
|
||||
@@ -32,35 +31,40 @@ export default function App() {
|
||||
<h1>Retro Claude Team Console</h1>
|
||||
</div>
|
||||
<div className="app-shell__meta">
|
||||
<span>LIVE STREAM</span>
|
||||
<span>TEAM COMMS</span>
|
||||
<span>PIXEL MODE</span>
|
||||
<span>LINK: {connected ? "ONLINE" : "OFFLINE"}</span>
|
||||
<span>SESSION: {String(session.status || "idle").toUpperCase()}</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>
|
||||
|
||||
<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}
|
||||
|
||||
<div className="console-grid">
|
||||
<div className="console-grid__side">
|
||||
<TeamBoard chat={chat} />
|
||||
</div>
|
||||
<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
|
||||
disabled={busy || session.status !== "running"}
|
||||
onSubmit={(prompt) => runAction(() => sendPrompt(prompt))}
|
||||
/>
|
||||
</div>
|
||||
<div className="console-grid__side">
|
||||
<TeamBoard chat={chat} />
|
||||
</div>
|
||||
</div>
|
||||
</ShellFrame>
|
||||
</main>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import PanelFrame from "./PanelFrame.jsx";
|
||||
|
||||
export default function ChatStream({ chat, session }) {
|
||||
export default function ChatStream({ chat, session, headerExtra = null }) {
|
||||
const scrollerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -16,7 +16,12 @@ export default function ChatStream({ chat, session }) {
|
||||
const isEmpty = !chat.trim();
|
||||
|
||||
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}>
|
||||
{isEmpty ? (
|
||||
<div className="empty-state">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default function PanelFrame({ title, eyebrow, children, className = "" }) {
|
||||
export default function PanelFrame({ title, eyebrow, children, className = "", headerExtra = null }) {
|
||||
return (
|
||||
<section className={`panel-frame ${className}`}>
|
||||
<div className="panel-frame__header">
|
||||
@@ -6,6 +6,7 @@ export default function PanelFrame({ title, eyebrow, children, className = "" })
|
||||
<p className="panel-frame__eyebrow">{eyebrow}</p>
|
||||
<h2 className="panel-frame__title">{title}</h2>
|
||||
</div>
|
||||
{headerExtra ? <div className="panel-frame__extra">{headerExtra}</div> : null}
|
||||
</div>
|
||||
<div className="panel-frame__body">{children}</div>
|
||||
</section>
|
||||
|
||||
@@ -5,7 +5,7 @@ export default function SessionToolbar({ session, busy, onStart, onActivate, onS
|
||||
const isStarting = session.status === "starting";
|
||||
|
||||
return (
|
||||
<div className="session-toolbar">
|
||||
<div className="session-toolbar session-toolbar--inline">
|
||||
<PixelButton tone="green" disabled={busy || isRunning || isStarting} onClick={onStart}>
|
||||
Start Session
|
||||
</PixelButton>
|
||||
|
||||
@@ -81,6 +81,8 @@ function isContinuationLine(line) {
|
||||
}
|
||||
|
||||
return [
|
||||
/^[•\-]\s+/.test(trimmed),
|
||||
/^[0-9]+\.\s+/.test(trimmed),
|
||||
/^[A-Za-zÀ-ÿ0-9ÇĞİÖŞÜçğıöşü"'`(]/.test(trimmed),
|
||||
/^[.!?…]/.test(trimmed),
|
||||
/^💪|^😊|^🚀|^☕|^🎨|^📱/.test(trimmed)
|
||||
@@ -115,7 +117,9 @@ export function parseTeamFeed(chat) {
|
||||
if (speakerMatch) {
|
||||
const member = memberMap.get(normalizeSpeaker(speakerMatch[1]));
|
||||
if (!member) {
|
||||
currentEntry = null;
|
||||
if (currentEntry && isContinuationLine(line)) {
|
||||
currentEntry.text = currentEntry.text ? `${currentEntry.text}\n${line}` : line;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
align-items: end;
|
||||
margin-bottom: 24px;
|
||||
align-items: start;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.app-shell__eyebrow,
|
||||
@@ -39,14 +39,15 @@
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.app-shell__meta span {
|
||||
padding: 8px 12px;
|
||||
border: 2px solid var(--border-mid);
|
||||
background: rgba(15, 22, 17, 0.8);
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(70, 121, 84, 0.45);
|
||||
background: rgba(15, 22, 17, 0.56);
|
||||
color: var(--text-dim);
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.shell-frame {
|
||||
@@ -80,14 +81,6 @@
|
||||
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,
|
||||
.prompt-composer,
|
||||
.error-banner {
|
||||
@@ -96,15 +89,6 @@
|
||||
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 {
|
||||
color: var(--accent-green);
|
||||
}
|
||||
@@ -125,7 +109,11 @@
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.session-toolbar--inline {
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pixel-button {
|
||||
@@ -191,6 +179,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.9fr);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.console-grid__main,
|
||||
@@ -201,14 +190,30 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.console-grid__side {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.console-grid__main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-frame__header {
|
||||
padding: 14px 16px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.panel-frame__body {
|
||||
padding: 14px 16px 16px;
|
||||
}
|
||||
|
||||
.panel-frame__extra {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-stream,
|
||||
.team-card__body {
|
||||
min-height: 420px;
|
||||
@@ -281,13 +286,16 @@
|
||||
|
||||
.team-board {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.team-card {
|
||||
border: 2px solid var(--border-mid);
|
||||
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);
|
||||
min-height: 348px;
|
||||
}
|
||||
|
||||
.team-card__header {
|
||||
@@ -342,8 +350,8 @@
|
||||
}
|
||||
|
||||
.team-card__body {
|
||||
min-height: 124px;
|
||||
max-height: 190px;
|
||||
min-height: 276px;
|
||||
max-height: 276px;
|
||||
}
|
||||
|
||||
.team-message {
|
||||
@@ -373,11 +381,11 @@
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.status-strip {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
.console-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.console-grid {
|
||||
.team-board {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -393,12 +401,17 @@
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.status-strip {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.shell-frame__screen {
|
||||
min-height: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.panel-frame__header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.session-toolbar--inline {
|
||||
justify-content: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user