From e7bf848fa551f1836ed4ec3f85bdd6676a52af8b Mon Sep 17 00:00:00 2001
From: XTRA_xty <3212559507@qq.com>
Date: Thu, 4 Jun 2026 10:44:15 +0800
Subject: [PATCH] Add GitHub chat panel
---
README.md | 8 +-
github-chat.css | 215 +++++++++++++++++++++++++++++++
github-chat.js | 331 ++++++++++++++++++++++++++++++++++++++++++++++++
manifest.json | 16 ++-
4 files changed, 567 insertions(+), 3 deletions(-)
create mode 100644 github-chat.css
create mode 100644 github-chat.js
diff --git a/README.md b/README.md
index 0af0dd3..d2d065c 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,17 @@
# GitHub Stories
-View stories on your GitHub dashboard.
+View stories on your GitHub dashboard and keep local chat notes on GitHub pages.
No uploads required. Just [install](#how-to-install) and visit [github.com](https://github.com).

+## GitHub Chat
+
+The extension adds a `Chat` button in the lower-right corner of every `github.com` page. Open it to write page-specific notes, review previous messages, or clear the current page history.
+
+Messages are stored locally with `chrome.storage.local`. Repository pages share one chat thread per `owner/repo`, while GitHub system pages use their page path. The panel also updates when GitHub changes pages without a full reload.
+
## How to Install
1. [Download ZIP](https://github.com/inquid/github-stories/archive/master.zip) and unzip on your computer.
diff --git a/github-chat.css b/github-chat.css
new file mode 100644
index 0000000..b16b9fe
--- /dev/null
+++ b/github-chat.css
@@ -0,0 +1,215 @@
+#github-stories-chat-root {
+ color-scheme: light;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ position: fixed;
+ z-index: 2147483647;
+}
+
+#github-stories-chat-root * {
+ box-sizing: border-box;
+}
+
+.ghs-chat-toggle {
+ align-items: center;
+ background: #24292f;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ border-radius: 999px;
+ bottom: 24px;
+ box-shadow: 0 10px 28px rgba(31, 35, 40, 0.25);
+ color: #ffffff;
+ cursor: pointer;
+ display: inline-flex;
+ font-size: 14px;
+ font-weight: 600;
+ gap: 8px;
+ line-height: 1;
+ min-height: 44px;
+ padding: 0 15px;
+ position: fixed;
+ right: 24px;
+}
+
+.ghs-chat-toggle:focus-visible,
+.ghs-chat-panel button:focus-visible,
+.ghs-chat-input:focus {
+ outline: 2px solid #0969da;
+ outline-offset: 2px;
+}
+
+.ghs-chat-count {
+ align-items: center;
+ background: #ffffff;
+ border-radius: 999px;
+ color: #24292f;
+ display: inline-flex;
+ font-size: 12px;
+ justify-content: center;
+ min-width: 22px;
+ padding: 4px 6px;
+}
+
+.ghs-chat-panel {
+ background: #ffffff;
+ border: 1px solid #d0d7de;
+ border-radius: 8px;
+ bottom: 78px;
+ box-shadow: 0 16px 48px rgba(31, 35, 40, 0.22);
+ display: flex;
+ flex-direction: column;
+ max-height: min(560px, calc(100vh - 110px));
+ overflow: hidden;
+ position: fixed;
+ right: 24px;
+ width: min(380px, calc(100vw - 32px));
+}
+
+.ghs-chat-hidden {
+ display: none;
+}
+
+.ghs-chat-header {
+ align-items: flex-start;
+ background: #f6f8fa;
+ border-bottom: 1px solid #d8dee4;
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 12px 14px;
+}
+
+.ghs-chat-header strong {
+ color: #24292f;
+ display: block;
+ font-size: 14px;
+ line-height: 20px;
+}
+
+.ghs-chat-context {
+ color: #57606a;
+ display: block;
+ font-size: 12px;
+ line-height: 18px;
+ max-width: 220px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.ghs-chat-actions {
+ display: flex;
+ gap: 6px;
+}
+
+.ghs-chat-actions button,
+.ghs-chat-send {
+ background: #f6f8fa;
+ border: 1px solid #d0d7de;
+ border-radius: 6px;
+ color: #24292f;
+ cursor: pointer;
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 18px;
+ padding: 4px 8px;
+}
+
+.ghs-chat-actions button:disabled {
+ color: #8c959f;
+ cursor: default;
+}
+
+.ghs-chat-close {
+ min-width: 28px;
+}
+
+.ghs-chat-messages {
+ background: #ffffff;
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ gap: 10px;
+ min-height: 170px;
+ overflow-y: auto;
+ padding: 14px;
+}
+
+.ghs-chat-empty {
+ color: #57606a;
+ font-size: 13px;
+ margin: auto;
+ text-align: center;
+}
+
+.ghs-chat-message {
+ background: #f6f8fa;
+ border: 1px solid #d8dee4;
+ border-radius: 8px;
+ color: #24292f;
+ padding: 9px 10px;
+}
+
+.ghs-chat-message-meta {
+ color: #57606a;
+ font-size: 11px;
+ line-height: 16px;
+ margin-bottom: 4px;
+}
+
+.ghs-chat-message-body {
+ font-size: 13px;
+ line-height: 19px;
+ overflow-wrap: anywhere;
+ white-space: pre-wrap;
+}
+
+.ghs-chat-form {
+ border-top: 1px solid #d8dee4;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 12px;
+}
+
+.ghs-chat-input {
+ border: 1px solid #d0d7de;
+ border-radius: 6px;
+ color: #24292f;
+ font: inherit;
+ font-size: 13px;
+ line-height: 19px;
+ min-height: 74px;
+ padding: 8px;
+ resize: vertical;
+}
+
+.ghs-chat-footer {
+ align-items: center;
+ display: flex;
+ gap: 12px;
+ justify-content: space-between;
+}
+
+.ghs-chat-status {
+ color: #57606a;
+ font-size: 12px;
+ line-height: 18px;
+}
+
+.ghs-chat-send {
+ background: #1f883d;
+ border-color: #1f883d;
+ color: #ffffff;
+ min-width: 64px;
+}
+
+@media (max-width: 480px) {
+ .ghs-chat-panel {
+ bottom: 72px;
+ right: 16px;
+ }
+
+ .ghs-chat-toggle {
+ bottom: 18px;
+ right: 16px;
+ }
+}
diff --git a/github-chat.js b/github-chat.js
new file mode 100644
index 0000000..8532221
--- /dev/null
+++ b/github-chat.js
@@ -0,0 +1,331 @@
+(() => {
+ const ROOT_ID = 'github-stories-chat-root';
+ const STORAGE_PREFIX = 'githubStoriesChat';
+ const MAX_MESSAGES = 100;
+ const MAX_MESSAGE_LENGTH = 2000;
+
+ if (window.location.hostname !== 'github.com') {
+ return;
+ }
+
+ if (document.getElementById(ROOT_ID)) {
+ return;
+ }
+
+ const state = {
+ context: getChatContext(),
+ messages: [],
+ isOpen: false,
+ lastLocation: window.location.href,
+ };
+
+ const root = document.createElement('div');
+ root.id = ROOT_ID;
+ root.innerHTML = `
+
+
+ `;
+
+ document.body.appendChild(root);
+
+ const elements = {
+ toggle: root.querySelector('.ghs-chat-toggle'),
+ panel: root.querySelector('.ghs-chat-panel'),
+ count: root.querySelector('.ghs-chat-count'),
+ context: root.querySelector('.ghs-chat-context'),
+ messages: root.querySelector('.ghs-chat-messages'),
+ form: root.querySelector('.ghs-chat-form'),
+ input: root.querySelector('.ghs-chat-input'),
+ status: root.querySelector('.ghs-chat-status'),
+ send: root.querySelector('.ghs-chat-send'),
+ clear: root.querySelector('.ghs-chat-clear'),
+ close: root.querySelector('.ghs-chat-close'),
+ };
+
+ elements.toggle.addEventListener('click', () => setPanelOpen(!state.isOpen));
+ elements.close.addEventListener('click', () => setPanelOpen(false));
+ elements.clear.addEventListener('click', clearMessages);
+ elements.form.addEventListener('submit', handleSubmit);
+ elements.input.addEventListener('keydown', handleInputKeydown);
+
+ document.addEventListener('turbo:load', syncLocationContext);
+ document.addEventListener('pjax:end', syncLocationContext);
+ window.addEventListener('popstate', syncLocationContext);
+ window.addEventListener('hashchange', syncLocationContext);
+ window.setInterval(syncLocationContext, 800);
+
+ updateContextLabel();
+ loadMessages();
+
+ async function handleSubmit(event) {
+ event.preventDefault();
+
+ const text = elements.input.value.trim();
+ if (!text) {
+ setStatus('Write a message before sending.');
+ return;
+ }
+
+ const message = {
+ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
+ text: text.slice(0, MAX_MESSAGE_LENGTH),
+ createdAt: new Date().toISOString(),
+ };
+
+ state.messages = [...state.messages, message].slice(-MAX_MESSAGES);
+ await saveMessages();
+ elements.input.value = '';
+ renderMessages();
+ setStatus('Message saved locally.');
+ }
+
+ function handleInputKeydown(event) {
+ if (event.key === 'Enter' && !event.shiftKey) {
+ event.preventDefault();
+ elements.form.requestSubmit();
+ }
+ }
+
+ async function clearMessages() {
+ if (!state.messages.length) {
+ setStatus('No messages to clear.');
+ return;
+ }
+
+ if (!window.confirm('Clear this page chat history?')) {
+ return;
+ }
+
+ state.messages = [];
+ await removeMessages();
+ renderMessages();
+ setStatus('Chat history cleared.');
+ }
+
+ function setPanelOpen(isOpen) {
+ state.isOpen = isOpen;
+ elements.panel.classList.toggle('ghs-chat-hidden', !isOpen);
+ elements.toggle.setAttribute('aria-expanded', String(isOpen));
+
+ if (isOpen) {
+ loadMessages().then(() => {
+ elements.input.focus();
+ });
+ }
+ }
+
+ async function loadMessages() {
+ const value = await storageGet(getStorageKey());
+ state.messages = Array.isArray(value) ? value.slice(-MAX_MESSAGES) : [];
+ renderMessages();
+ }
+
+ async function saveMessages() {
+ await storageSet(getStorageKey(), state.messages);
+ }
+
+ async function removeMessages() {
+ await storageRemove(getStorageKey());
+ }
+
+ function renderMessages() {
+ elements.count.textContent = String(state.messages.length);
+ elements.clear.disabled = state.messages.length === 0;
+ elements.messages.replaceChildren();
+
+ if (!state.messages.length) {
+ const empty = document.createElement('p');
+ empty.className = 'ghs-chat-empty';
+ empty.textContent = 'No messages yet for this page.';
+ elements.messages.appendChild(empty);
+ return;
+ }
+
+ state.messages.forEach((message) => {
+ const article = document.createElement('article');
+ article.className = 'ghs-chat-message';
+
+ const meta = document.createElement('div');
+ meta.className = 'ghs-chat-message-meta';
+ meta.textContent = `You - ${formatTimestamp(message.createdAt)}`;
+
+ const body = document.createElement('div');
+ body.className = 'ghs-chat-message-body';
+ body.textContent = message.text;
+
+ article.appendChild(meta);
+ article.appendChild(body);
+ elements.messages.appendChild(article);
+ });
+
+ elements.messages.scrollTop = elements.messages.scrollHeight;
+ }
+
+ function syncLocationContext() {
+ if (state.lastLocation === window.location.href) {
+ return;
+ }
+
+ state.lastLocation = window.location.href;
+ state.context = getChatContext();
+ updateContextLabel();
+ loadMessages();
+ }
+
+ function updateContextLabel() {
+ elements.context.textContent = state.context.label;
+ }
+
+ function getStorageKey() {
+ return `${STORAGE_PREFIX}:${state.context.key}`;
+ }
+
+ function setStatus(message) {
+ elements.status.textContent = message;
+ }
+
+ function getChatContext() {
+ const parts = window.location.pathname.split('/').filter(Boolean);
+
+ if (parts.length >= 2 && !isGitHubSystemPath(parts[0])) {
+ const repoKey = `${parts[0]}/${parts[1]}`;
+ return {
+ key: `repo:${repoKey}`,
+ label: repoKey,
+ };
+ }
+
+ const page = window.location.pathname === '/' ? 'home' : window.location.pathname;
+ return {
+ key: `page:${page}`,
+ label: page === 'home' ? 'github.com home' : page,
+ };
+ }
+
+ function isGitHubSystemPath(segment) {
+ return [
+ 'dashboard',
+ 'explore',
+ 'marketplace',
+ 'notifications',
+ 'organizations',
+ 'pulls',
+ 'settings',
+ 'topics',
+ 'trending',
+ ].includes(segment);
+ }
+
+ function formatTimestamp(value) {
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) {
+ return 'just now';
+ }
+
+ return date.toLocaleString([], {
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ }
+
+ function hasChromeStorage() {
+ return (
+ typeof chrome !== 'undefined' &&
+ chrome.storage &&
+ chrome.storage.local
+ );
+ }
+
+ function storageGet(key) {
+ if (!hasChromeStorage()) {
+ return Promise.resolve(readLocalStorage(key));
+ }
+
+ return new Promise((resolve) => {
+ chrome.storage.local.get([key], (result) => {
+ if (chrome.runtime.lastError) {
+ resolve(readLocalStorage(key));
+ return;
+ }
+
+ resolve(result[key]);
+ });
+ });
+ }
+
+ function storageSet(key, value) {
+ if (!hasChromeStorage()) {
+ writeLocalStorage(key, value);
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve) => {
+ chrome.storage.local.set({ [key]: value }, () => {
+ if (chrome.runtime.lastError) {
+ writeLocalStorage(key, value);
+ }
+
+ resolve();
+ });
+ });
+ }
+
+ function storageRemove(key) {
+ if (!hasChromeStorage()) {
+ window.localStorage.removeItem(key);
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve) => {
+ chrome.storage.local.remove(key, () => {
+ if (chrome.runtime.lastError) {
+ window.localStorage.removeItem(key);
+ }
+
+ resolve();
+ });
+ });
+ }
+
+ function readLocalStorage(key) {
+ try {
+ return JSON.parse(window.localStorage.getItem(key) || '[]');
+ } catch (error) {
+ return [];
+ }
+ }
+
+ function writeLocalStorage(key, value) {
+ window.localStorage.setItem(key, JSON.stringify(value));
+ }
+})();
diff --git a/manifest.json b/manifest.json
index db0a183..5230b35 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"name": "GitHub Stories",
- "version": "0.1",
- "description": "Stories on GitHub",
+ "version": "0.2",
+ "description": "Stories and local chat notes on GitHub",
"permissions": [
"activeTab",
"declarativeContent",
@@ -27,6 +27,18 @@
"story-list.css",
"story-view.css"
]
+ },
+ {
+ "matches": [
+ "https://github.com/*"
+ ],
+ "js": [
+ "github-chat.js"
+ ],
+ "css": [
+ "github-chat.css"
+ ],
+ "run_at": "document_idle"
}
],
"icons": {