diff --git a/css/index.css b/css/index.css index f54b07c..14514da 100644 --- a/css/index.css +++ b/css/index.css @@ -165,3 +165,175 @@ text-shadow: -1px 1px #417cb8; border: none; } + +/* URL入力とClaude Codeボタンの行 */ +.URLInputRow { + display: flex; + align-items: center; + gap: 8px; + margin: 0.1em 0; +} + +.URLInputRow .URLInput { + flex: 1; + margin: 0; +} + +/* Claude Codeボタン */ +.ClaudeCodeButton { + padding: 6px 8px; + font-size: 12px; + font-weight: bold; + border: 1px solid #ccc; + border-radius: 4px; + background-color: #f5f5f5; + color: #666; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + min-width: 36px; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 2px; +} + +.ClaudeCodeButton svg { + vertical-align: middle; +} + +.ClaudeCodeButton:hover:not(:disabled) { + background-color: #e0e0e0; + border-color: #999; +} + +.ClaudeCodeButton:disabled { + cursor: not-allowed; + opacity: 0.7; +} + +.ClaudeCodeButton--loading { + background-color: #fff3cd; + border-color: #ffc107; + color: #856404; +} + +.ClaudeCodeButton--complete { + background-color: #d4edda; + border-color: #28a745; + color: #155724; +} + +.ClaudeCodeButton--complete:hover:not(:disabled) { + background-color: #c3e6cb; + border-color: #1e7e34; +} + +.ClaudeCodeButton--error { + background-color: #f8d7da; + border-color: #dc3545; + color: #721c24; +} + +.ClaudeCodeButton-loading { + display: flex; + align-items: center; + gap: 4px; +} + +.ClaudeCodeButton-spinner { + display: inline-block; + width: 12px; + height: 12px; + border: 2px solid #856404; + border-top-color: transparent; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Claude Code Preview */ +.ClaudeCodePreview { + margin: 0.5em 0; + padding: 8px 12px; + border-radius: 4px; + font-size: 13px; +} + +.ClaudeCodePreview-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; + font-weight: bold; + font-size: 12px; +} + +.ClaudeCodePreview-hint { + font-weight: normal; + color: #666; + font-size: 11px; +} + +.ClaudeCodePreview-close { + margin-left: auto; + background: none; + border: none; + font-size: 16px; + cursor: pointer; + color: #999; + padding: 0 4px; +} + +.ClaudeCodePreview-close:hover { + color: #333; +} + +.ClaudeCodePreview-content { + white-space: pre-wrap; + line-height: 1.5; + font-family: Monaco, "Andale Mono", monospace; +} + +.ClaudeCodePreview--loading { + background-color: #fff8e1; + border: 1px solid #ffcc02; +} + +.ClaudeCodePreview--loading .ClaudeCodePreview-spinner { + display: inline-block; + width: 12px; + height: 12px; + border: 2px solid #f0a000; + border-top-color: transparent; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.ClaudeCodePreview--complete { + background-color: #e8f5e9; + border: 1px solid #4caf50; +} + +.ClaudeCodePreview--complete .ClaudeCodePreview-content { + cursor: pointer; +} + +.ClaudeCodePreview--complete .ClaudeCodePreview-content:hover { + background-color: rgba(76, 175, 80, 0.1); +} + +.ClaudeCodePreview--error { + background-color: #ffebee; + border: 1px solid #f44336; +} + +.ClaudeCodePreview--error .ClaudeCodePreview-content { + color: #c62828; +} diff --git a/src/browser/Action/ServiceAction.js b/src/browser/Action/ServiceAction.js index 9ec2328..da625d0 100644 --- a/src/browser/Action/ServiceAction.js +++ b/src/browser/Action/ServiceAction.js @@ -8,6 +8,8 @@ import notie from "notie"; import { show as LoadingShow, dismiss as LoadingDismiss } from "../view-util/Loading"; import RelatedItemModel from "../models/RelatedItemModel"; import serviceInstance from "../service-instance"; +import { spawn } from "child_process"; +import fs from "fs"; export default class ServiceAction extends Action { fetchTags(service) { @@ -191,4 +193,60 @@ export default class ServiceAction extends Action { resetField() { this.dispatch(keys.resetField); } + + // Claude Code関連アクション + runClaudeCode(url, config) { + if (!config?.enabled) return; + if (!fs.existsSync(config.cliPath)) return; + if (!fs.existsSync(config.workDir)) { + this.dispatch(keys.claudeCodeError, { url, error: `WorkDir not found: ${config.workDir}` }); + return; + } + + this.dispatch(keys.claudeCodeStart, { url }); + + const args = []; + if (config.mcpConfig) { + args.push("--mcp-config", JSON.stringify(config.mcpConfig)); + } + args.push("--print", "--dangerously-skip-permissions", `${config.prompt}\n\nURL: ${url}`); + + const claudeProcess = spawn(config.cliPath, args, { + cwd: config.workDir, + env: { + ...process.env, + PATH: "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:" + process.env.PATH + }, + shell: false, + stdio: ["ignore", "pipe", "pipe"] + }); + + let stdout = ""; + let stderr = ""; + + claudeProcess.stdout.on("data", (data) => (stdout += data.toString())); + claudeProcess.stderr.on("data", (data) => (stderr += data.toString())); + + claudeProcess.on("close", (code) => { + if (code === 0 && stdout) { + const match = stdout.match(/```(?:markdown)?\s*([\s\S]*?)```/); + const result = match ? match[1].trim() : stdout.trim(); + this.dispatch(keys.claudeCodeComplete, { url, result }); + } else { + this.dispatch(keys.claudeCodeError, { url, error: stderr || `Exit code: ${code}` }); + } + }); + + claudeProcess.on("error", (error) => { + this.dispatch(keys.claudeCodeError, { url, error: error.message }); + }); + } + + clearClaudeCodeResult() { + this.dispatch(keys.claudeCodeClear); + } + + insertClaudeCodeResult() { + this.dispatch(keys.claudeCodeInsert); + } } diff --git a/src/browser/Action/ServiceActionConst.js b/src/browser/Action/ServiceActionConst.js index 0495166..4afa691 100644 --- a/src/browser/Action/ServiceActionConst.js +++ b/src/browser/Action/ServiceActionConst.js @@ -14,5 +14,11 @@ export default { removeRelatedItem: Symbol("removeRelatedItem"), finishEditingRelatedItem: Symbol("finishEditingRelatedItem"), enableService: Symbol("enableService"), - disableService: Symbol("disableService") + disableService: Symbol("disableService"), + // Claude Code関連 + claudeCodeStart: Symbol("claudeCodeStart"), + claudeCodeComplete: Symbol("claudeCodeComplete"), + claudeCodeError: Symbol("claudeCodeError"), + claudeCodeClear: Symbol("claudeCodeClear"), + claudeCodeInsert: Symbol("claudeCodeInsert") }; diff --git a/src/browser/App.js b/src/browser/App.js index bcc3b39..d17d140 100644 --- a/src/browser/App.js +++ b/src/browser/App.js @@ -10,8 +10,10 @@ import TitleInput from "./component/TitleInput"; import SubmitButton from "./component/SubmitButton"; import RelatedListBox from "./component/RelatedListBox"; import ServiceList from "./component/ServiceList"; +import ClaudeCodeButton from "./component/ClaudeCodeButton"; +import ClaudeCodePreview from "./component/ClaudeCodePreview"; import AppContext from "./AppContext"; -import serviceManger, { waitForInitialization } from "./service-instance"; +import serviceManger, { waitForInitialization, getClaudeCodeConfig } from "./service-instance"; const ipcRenderer = require("electron").ipcRenderer; const appContext = new AppContext(); @@ -20,6 +22,7 @@ class App extends React.Component { constructor(...args) { super(...args); this._TagSelect = null; + this._claudeCodeConfig = getClaudeCodeConfig(); this.state = Object.assign( { initialized: false @@ -70,7 +73,9 @@ class App extends React.Component { const service = serviceManger.getTagService(); if (service && state.selectedTags.length === 0 && state.comment.length === 0) { appContext.ServiceAction.fetchContent(service, URL) - .then(({ comment, tags, relatedItems }) => { + .then((result) => { + if (!result) return; + const { comment, tags, relatedItems } = result; if (comment) { appContext.ServiceAction.updateComment(comment); } @@ -187,6 +192,18 @@ class App extends React.Component { ServiceAction.addRelatedItem(); }; const submitPostLink = this.postLink.bind(this); + + // Claude Code関連 + const runClaudeCode = (url, config) => { + ServiceAction.runClaudeCode(url, config); + }; + const insertClaudeCodeResult = () => { + ServiceAction.insertClaudeCodeResult(); + }; + const clearClaudeCodeResult = () => { + ServiceAction.clearClaudeCodeResult(); + }; + return (