diff --git a/pty_session.go b/pty_session.go index 5e1a938..4a48a7d 100644 --- a/pty_session.go +++ b/pty_session.go @@ -221,17 +221,18 @@ func NewPTYSessionManager() *PTYSessionManager { } // GetOrCreate returns an existing session or creates a new one. -func (m *PTYSessionManager) GetOrCreate(project, projectDir string) (*PTYSession, bool, error) { +// sessionKey is a free-form string (e.g. "project:tabId"). +func (m *PTYSessionManager) GetOrCreate(sessionKey, projectDir string) (*PTYSession, bool, error) { m.mu.Lock() defer m.mu.Unlock() - if sess, ok := m.sessions[project]; ok { + if sess, ok := m.sessions[sessionKey]; ok { // Check if process is still alive select { case <-sess.Done(): // Process exited, remove and create new sess.Close() - delete(m.sessions, project) + delete(m.sessions, sessionKey) default: sess.mu.Lock() sess.lastActive = time.Now() @@ -245,7 +246,7 @@ func (m *PTYSessionManager) GetOrCreate(project, projectDir string) (*PTYSession return nil, false, err } - m.sessions[project] = sess + m.sessions[sessionKey] = sess return sess, true, nil } diff --git a/templates/chat.html b/templates/chat.html index 3eb4c50..d96712f 100644 --- a/templates/chat.html +++ b/templates/chat.html @@ -16,6 +16,7 @@ --text-muted: #6c6c80; --accent: #e94560; --accent-hover: #ff6b81; + --error: #f44336; } body { background: var(--bg-primary); @@ -141,7 +142,99 @@ cursor: pointer; } .terminal-header button:hover { border-color: var(--accent); color: var(--text-primary); } - #terminal-container { flex: 1; overflow: hidden; } + + /* Tab bar */ + .tab-bar { + display: flex; + align-items: center; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + padding: 0 0.25rem; + flex-shrink: 0; + overflow-x: auto; + gap: 2px; + min-height: 30px; + } + .tab-bar::-webkit-scrollbar { height: 0; } + .tab { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.2rem 0.5rem; + font-size: 0.7rem; + color: var(--text-muted); + cursor: pointer; + border: 1px solid transparent; + border-bottom: none; + border-radius: 4px 4px 0 0; + white-space: nowrap; + user-select: none; + transition: color 0.15s, background 0.15s; + } + .tab:hover { color: var(--text-secondary); background: rgba(255,255,255,0.03); } + .tab.active { color: var(--text-primary); background: var(--bg-primary); border-color: var(--border); } + .tab-close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + font-size: 0.65rem; + border-radius: 3px; + color: var(--text-muted); + transition: background 0.1s, color 0.1s; + } + .tab-close:hover { background: rgba(244,67,54,0.3); color: var(--error); } + .tab-add-wrap { position: relative; flex-shrink: 0; } + .tab-add { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + font-size: 0.85rem; + color: var(--text-muted); + cursor: pointer; + border-radius: 4px; + border: none; + background: none; + font-family: inherit; + transition: color 0.15s, background 0.15s; + } + .tab-add:hover { color: var(--text-primary); background: rgba(255,255,255,0.05); } + .tab-dropdown { + position: absolute; + top: 100%; + left: 0; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 6px; + min-width: 180px; + max-height: 300px; + overflow-y: auto; + z-index: 100; + box-shadow: 0 4px 12px rgba(0,0,0,0.4); + } + .tab-dropdown.hidden { display: none; } + .tab-dropdown-item { + display: block; + width: 100%; + padding: 0.4rem 0.75rem; + font-size: 0.75rem; + font-family: inherit; + color: var(--text-secondary); + background: none; + border: none; + text-align: left; + cursor: pointer; + transition: background 0.1s, color 0.1s; + } + .tab-dropdown-item:hover { background: rgba(233,69,96,0.15); color: var(--text-primary); } + + /* Terminal containers */ + #terminals { flex: 1; overflow: hidden; position: relative; } + .term-pane { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; } + .term-pane.active { display: block; } .xterm { height: 100%; } /* File viewer overlay */ @@ -201,6 +294,47 @@ .file-viewer-content th { background: var(--bg-secondary); color: var(--accent); font-weight: 600; } .file-viewer-content tr:nth-child(even) { background: rgba(22,27,34,0.5); } + /* Sidebar project list */ + .sidebar-projects-list { max-height: 180px; overflow-y: auto; } + .sidebar-projects-list .file-item.active { + color: var(--accent); + background: rgba(233,69,96,0.1); + font-weight: 600; + } + .sidebar-add-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + font-size: 0.8rem; + color: var(--text-muted); + cursor: pointer; + border-radius: 3px; + transition: color 0.15s, background 0.15s; + line-height: 1; + } + .sidebar-add-btn:hover { color: var(--accent); background: rgba(233,69,96,0.15); } + + /* Create project modal */ + .create-project-modal { + position: fixed; top: 0; left: 0; width: 100%; height: 100%; + background: rgba(0,0,0,0.5); display: flex; align-items: center; + justify-content: center; z-index: 300; + } + .create-project-box { + background: var(--bg-secondary); border: 1px solid var(--border); + border-radius: 8px; padding: 1rem; width: 260px; + box-shadow: 0 4px 16px rgba(0,0,0,0.4); + } + .create-project-box h3 { color: var(--accent); font-size: 0.85rem; margin-bottom: 0.6rem; } + .create-project-box input { + width: 100%; padding: 0.35rem 0.5rem; background: var(--bg-primary); + border: 1px solid var(--border); border-radius: 4px; color: var(--text-primary); + font-size: 0.75rem; font-family: inherit; outline: none; + } + .create-project-box input:focus { border-color: var(--accent); } + /* Scrollbar */ ::-webkit-scrollbar { width: 5px; } ::-webkit-scrollbar-track { background: transparent; } @@ -215,6 +349,20 @@