' +
@@ -201,6 +216,86 @@ define(function (require, exports, module) {
_autoScroll = (el.scrollHeight - el.scrollTop - el.clientHeight) < 50;
});
+ // Context bar
+ $contextBar = $panel.find(".ai-chat-context-bar");
+
+ // Track editor selection/cursor for context chips
+ EditorManager.off("activeEditorChange.aiChat");
+ EditorManager.on("activeEditorChange.aiChat", function (_event, newEditor, oldEditor) {
+ if (oldEditor) {
+ oldEditor.off("cursorActivity.aiContext");
+ }
+ if (newEditor) {
+ newEditor.off("cursorActivity.aiContext");
+ newEditor.on("cursorActivity.aiContext", function (_evt, editor) {
+ _updateSelectionChip(editor);
+ });
+ }
+ _updateSelectionChip(newEditor);
+ });
+ // Bind to current editor if already active
+ const currentEditor = EditorManager.getActiveEditor();
+ if (currentEditor) {
+ currentEditor.off("cursorActivity.aiContext");
+ currentEditor.on("cursorActivity.aiContext", function (_evt, editor) {
+ _updateSelectionChip(editor);
+ });
+ }
+ _updateSelectionChip(currentEditor);
+
+ // Track live preview status — listen to both LiveDev status changes
+ // and panel show/hide events so the chip updates when the panel is closed
+ LiveDevMain.off("statusChange.aiChat");
+ LiveDevMain.on("statusChange.aiChat", _updateLivePreviewChip);
+ LiveDevMain.off(LiveDevMain.EVENT_OPEN_PREVIEW_URL + ".aiChat");
+ LiveDevMain.on(LiveDevMain.EVENT_OPEN_PREVIEW_URL + ".aiChat", function () {
+ _livePreviewDismissed = false;
+ _updateLivePreviewChip();
+ });
+ WorkspaceManager.off(WorkspaceManager.EVENT_WORKSPACE_PANEL_SHOWN + ".aiChat");
+ WorkspaceManager.on(WorkspaceManager.EVENT_WORKSPACE_PANEL_SHOWN + ".aiChat", _updateLivePreviewChip);
+ WorkspaceManager.off(WorkspaceManager.EVENT_WORKSPACE_PANEL_HIDDEN + ".aiChat");
+ WorkspaceManager.on(WorkspaceManager.EVENT_WORKSPACE_PANEL_HIDDEN + ".aiChat", _updateLivePreviewChip);
+ _updateLivePreviewChip();
+
+ // Refresh context bar when the AI tab becomes active (DOM updates
+ // are deferred while the tab is hidden to avoid layout interference)
+ SidebarTabs.off("tabChanged.aiChat");
+ SidebarTabs.on("tabChanged.aiChat", function (_event, tabId) {
+ if (tabId === "ai") {
+ _updateSelectionChip();
+ _updateLivePreviewChip();
+ }
+ });
+
+ // When a screenshot is captured, attach the image to the awaiting tool indicator
+ PhoenixConnectors.off("screenshotCaptured.aiChat");
+ PhoenixConnectors.on("screenshotCaptured.aiChat", function (_event, base64) {
+ const $tool = _$msgs().find('.ai-msg-tool').filter(function () {
+ return $(this).data("awaitingScreenshot");
+ }).last();
+ if ($tool.length) {
+ $tool.data("awaitingScreenshot", false);
+ const $detail = $tool.find(".ai-tool-detail");
+ const $img = $(' ');
+ $img.on("click", function (e) {
+ e.stopPropagation();
+ $img.toggleClass("expanded");
+ _scrollToBottom();
+ });
+ $img.on("load", function () {
+ // Force scroll — the image load changes height after insertion,
+ // which can cause the scroll listener to clear _autoScroll
+ if ($messages && $messages.length) {
+ $messages[0].scrollTop = $messages[0].scrollHeight;
+ }
+ });
+ $detail.html($img);
+ $tool.addClass("ai-tool-expanded");
+ _scrollToBottom();
+ }
+ });
+
SidebarTabs.addToTab("ai", $panel);
}
@@ -216,6 +311,156 @@ define(function (require, exports, module) {
SidebarTabs.addToTab("ai", $unavailable);
}
+ // --- Context bar chip management ---
+
+ /**
+ * Update the selection/cursor chip based on the active editor state.
+ * Skipped when the AI tab isn't active — calling getSelection()/getSelectedText()
+ * from both activeEditorChange and cursorActivity during inline editor operations
+ * interferes with the inline editor's cursor position tracking.
+ */
+ function _updateSelectionChip(editor) {
+ if (SidebarTabs.getActiveTab() !== "ai") {
+ return;
+ }
+ if (!editor) {
+ editor = EditorManager.getActiveEditor();
+ }
+ if (!editor) {
+ _lastSelectionInfo = null;
+ _lastCursorLine = null;
+ _lastCursorFile = null;
+ _renderContextBar();
+ return;
+ }
+
+ let filePath = editor.document.file.fullPath;
+ if (filePath.startsWith("/tauri/")) {
+ filePath = filePath.replace("/tauri", "");
+ }
+ const fileName = filePath.split("/").pop();
+
+ if (editor.hasSelection()) {
+ const sel = editor.getSelection();
+ const startLine = sel.start.line + 1;
+ const endLine = sel.end.line + 1;
+ const selectedText = editor.getSelectedText();
+
+ // Reset dismissed flag when selection changes
+ if (!_lastSelectionInfo ||
+ _lastSelectionInfo.startLine !== startLine ||
+ _lastSelectionInfo.endLine !== endLine ||
+ _lastSelectionInfo.filePath !== filePath) {
+ _selectionDismissed = false;
+ }
+
+ _lastSelectionInfo = {
+ filePath: filePath,
+ fileName: fileName,
+ startLine: startLine,
+ endLine: endLine,
+ selectedText: selectedText
+ };
+ _lastCursorLine = null;
+ _lastCursorFile = null;
+ } else {
+ const cursor = editor.getCursorPos();
+ const cursorLine = cursor.line + 1;
+ // Reset cursor dismissed when cursor moves to a different line
+ if (_cursorDismissed && _cursorDismissedLine !== cursorLine) {
+ _cursorDismissed = false;
+ }
+ _lastSelectionInfo = null;
+ _lastCursorLine = cursorLine;
+ _lastCursorFile = fileName;
+ }
+
+ _renderContextBar();
+ }
+
+ /**
+ * Update the live preview chip based on panel visibility.
+ */
+ function _updateLivePreviewChip() {
+ if (SidebarTabs.getActiveTab() !== "ai") {
+ return;
+ }
+ const panel = WorkspaceManager.getPanelForID("live-preview-panel");
+ const wasActive = _livePreviewActive;
+ _livePreviewActive = !!(panel && panel.isVisible());
+ // Reset dismissed when live preview is re-opened
+ if (_livePreviewActive && !wasActive) {
+ _livePreviewDismissed = false;
+ }
+ _renderContextBar();
+ }
+
+ /**
+ * Rebuild the context bar chips from current state.
+ */
+ function _renderContextBar() {
+ if (!$contextBar) {
+ return;
+ }
+ $contextBar.empty();
+
+ // Live preview chip
+ if (_livePreviewActive && !_livePreviewDismissed) {
+ const $lpChip = $(
+ '' +
+ ' ' +
+ '' + Strings.AI_CHAT_CONTEXT_LIVE_PREVIEW + ' ' +
+ '× ' +
+ ' '
+ );
+ $lpChip.find(".ai-context-chip-close").on("click", function () {
+ _livePreviewDismissed = true;
+ _renderContextBar();
+ });
+ $contextBar.append($lpChip);
+ }
+
+ // Selection or cursor chip
+ if (_lastSelectionInfo && !_selectionDismissed) {
+ const label = StringUtils.format(Strings.AI_CHAT_CONTEXT_SELECTION,
+ _lastSelectionInfo.startLine, _lastSelectionInfo.endLine) +
+ " in " + _lastSelectionInfo.fileName;
+ const $chip = $(
+ '' +
+ ' ' +
+ ' ' +
+ '× ' +
+ ' '
+ );
+ $chip.find(".ai-context-chip-label").text(label);
+ $chip.find(".ai-context-chip-close").on("click", function () {
+ _selectionDismissed = true;
+ _renderContextBar();
+ });
+ $contextBar.append($chip);
+ } else if (_lastCursorLine !== null && !_lastSelectionInfo && !_cursorDismissed) {
+ const label = StringUtils.format(Strings.AI_CHAT_CONTEXT_CURSOR, _lastCursorLine) +
+ " in " + _lastCursorFile;
+ const $cursorChip = $(
+ '' +
+ ' ' +
+ ' ' +
+ '× ' +
+ ' '
+ );
+ $cursorChip.find(".ai-context-chip-label").text(label);
+ $cursorChip.find(".ai-context-chip-close").on("click", function () {
+ _cursorDismissed = true;
+ _cursorDismissedLine = _lastCursorLine;
+ _renderContextBar();
+ });
+ $contextBar.append($cursorChip);
+ }
+
+ // Toggle visibility
+ $contextBar.toggleClass("has-chips", $contextBar.children().length > 0);
+ }
+
/**
* Send the current input as a message to Claude.
*/
@@ -258,11 +503,27 @@ define(function (require, exports, module) {
const prompt = text;
console.log("[AI UI] Sending prompt:", text.slice(0, 60));
+ // Gather selection context if available and not dismissed
+ let selectionContext = null;
+ if (_lastSelectionInfo && !_selectionDismissed && _lastSelectionInfo.selectedText) {
+ let selectedText = _lastSelectionInfo.selectedText;
+ if (selectedText.length > 10000) {
+ selectedText = selectedText.slice(0, 10000);
+ }
+ selectionContext = {
+ filePath: _lastSelectionInfo.filePath,
+ startLine: _lastSelectionInfo.startLine,
+ endLine: _lastSelectionInfo.endLine,
+ selectedText: selectedText
+ };
+ }
+
_nodeConnector.execPeer("sendPrompt", {
prompt: prompt,
projectPath: projectPath,
sessionAction: "continue",
- locale: brackets.getLocale()
+ locale: brackets.getLocale(),
+ selectionContext: selectionContext
}).then(function (result) {
_currentRequestId = result.requestId;
console.log("[AI UI] RequestId:", result.requestId);
@@ -298,6 +559,13 @@ define(function (require, exports, module) {
_isStreaming = false;
_firstEditInResponse = true;
_undoApplied = false;
+ _selectionDismissed = false;
+ _lastSelectionInfo = null;
+ _lastCursorLine = null;
+ _lastCursorFile = null;
+ _cursorDismissed = false;
+ _cursorDismissedLine = null;
+ _livePreviewDismissed = false;
SnapshotStore.reset();
PhoenixConnectors.clearPreviousContentMap();
if ($messages) {
@@ -350,7 +618,12 @@ define(function (require, exports, module) {
Edit: { icon: "fa-solid fa-pen", color: "#e8a838", label: Strings.AI_CHAT_TOOL_EDIT },
Write: { icon: "fa-solid fa-file-pen", color: "#e8a838", label: Strings.AI_CHAT_TOOL_WRITE },
Bash: { icon: "fa-solid fa-terminal", color: "#c084fc", label: Strings.AI_CHAT_TOOL_RUN_CMD },
- Skill: { icon: "fa-solid fa-puzzle-piece", color: "#e0c060", label: Strings.AI_CHAT_TOOL_SKILL }
+ Skill: { icon: "fa-solid fa-puzzle-piece", color: "#e0c060", label: Strings.AI_CHAT_TOOL_SKILL },
+ "mcp__phoenix-editor__getEditorState": { icon: "fa-solid fa-code", color: "#6bc76b", label: Strings.AI_CHAT_TOOL_EDITOR_STATE },
+ "mcp__phoenix-editor__takeScreenshot": { icon: "fa-solid fa-camera", color: "#c084fc", label: Strings.AI_CHAT_TOOL_SCREENSHOT },
+ "mcp__phoenix-editor__execJsInLivePreview": { icon: "fa-solid fa-eye", color: "#66bb6a", label: Strings.AI_CHAT_TOOL_LIVE_PREVIEW_JS },
+ "mcp__phoenix-editor__controlEditor": { icon: "fa-solid fa-code", color: "#6bc76b", label: Strings.AI_CHAT_TOOL_CONTROL_EDITOR },
+ TodoWrite: { icon: "fa-solid fa-list-check", color: "#66bb6a", label: Strings.AI_CHAT_TOOL_TASKS }
};
function _onProgress(_event, data) {
@@ -391,6 +664,42 @@ define(function (require, exports, module) {
}
}
+ /**
+ * Start an elapsed-time counter on a tool indicator. Called when the tool's
+ * stale timer fires (no streaming activity for 2s).
+ */
+ function _startElapsedTimer($tool) {
+ if ($tool.data("elapsedTimer")) {
+ return; // already running
+ }
+ const startTime = $tool.data("startTime") || Date.now();
+ const $header = $tool.find(".ai-tool-header");
+ let $elapsed = $header.find(".ai-tool-elapsed");
+ if (!$elapsed.length) {
+ $elapsed = $(' ');
+ $header.append($elapsed);
+ }
+ function update() {
+ const secs = Math.floor((Date.now() - startTime) / 1000);
+ if (secs < 60) {
+ $elapsed.text(secs + "s");
+ } else {
+ const m = Math.floor(secs / 60);
+ const s = secs % 60;
+ $elapsed.text(m + "m " + (s < 10 ? "0" : "") + s + "s");
+ }
+ }
+ update();
+ const timerId = setInterval(function () {
+ if ($tool.hasClass("ai-tool-done")) {
+ clearInterval(timerId);
+ return;
+ }
+ update();
+ }, 1000);
+ $tool.data("elapsedTimer", timerId);
+ }
+
function _onToolStream(_event, data) {
const uniqueToolId = (_currentRequestId || "") + "-" + data.toolId;
_traceToolStreamCounts[uniqueToolId] = (_traceToolStreamCounts[uniqueToolId] || 0) + 1;
@@ -436,6 +745,7 @@ define(function (require, exports, module) {
if ($livePreview.length && !$tool.hasClass("ai-tool-done")) {
$livePreview.text(phrases[idx]);
}
+ _startElapsedTimer($tool);
_toolStreamRotateTimer = setInterval(function () {
idx = (idx + 1) % phrases.length;
const $p = $tool.find(".ai-tool-preview");
@@ -490,7 +800,8 @@ define(function (require, exports, module) {
Edit: "new_string",
Bash: "command",
Grep: "pattern",
- Glob: "pattern"
+ Glob: "pattern",
+ "mcp__phoenix-editor__execJsInLivePreview": "code"
}[toolName];
if (!interestingKey) {
@@ -862,6 +1173,7 @@ define(function (require, exports, module) {
if ($target.length) {
try {
$target.html(marked.parse(_segmentText, { breaks: true, gfm: true }));
+ _enhanceColorCodes($target);
} catch (e) {
$target.text(_segmentText);
}
@@ -869,6 +1181,65 @@ define(function (require, exports, module) {
}
}
+ const HEX_COLOR_RE = /#[a-f0-9]{3,8}\b/gi;
+
+ /**
+ * Scan text nodes inside an element for hex color codes and wrap each
+ * with an inline swatch element showing the actual color.
+ */
+ function _enhanceColorCodes($el) {
+ // Walk text nodes inside the element, but skip blocks and already-enhanced swatches
+ const walker = document.createTreeWalker(
+ $el[0],
+ NodeFilter.SHOW_TEXT,
+ {
+ acceptNode: function (node) {
+ const parent = node.parentNode;
+ if (parent.closest && parent.closest("pre, .ai-color-swatch")) {
+ return NodeFilter.FILTER_REJECT;
+ }
+ return HEX_COLOR_RE.test(node.nodeValue)
+ ? NodeFilter.FILTER_ACCEPT
+ : NodeFilter.FILTER_REJECT;
+ }
+ }
+ );
+
+ const textNodes = [];
+ let n;
+ while ((n = walker.nextNode())) {
+ textNodes.push(n);
+ }
+
+ textNodes.forEach(function (textNode) {
+ const text = textNode.nodeValue;
+ HEX_COLOR_RE.lastIndex = 0;
+ const frag = document.createDocumentFragment();
+ let lastIndex = 0;
+ let match;
+ while ((match = HEX_COLOR_RE.exec(text)) !== null) {
+ // Append any text before the match
+ if (match.index > lastIndex) {
+ frag.appendChild(document.createTextNode(text.slice(lastIndex, match.index)));
+ }
+ const color = match[0];
+ const swatch = document.createElement("span");
+ swatch.className = "ai-color-swatch";
+ swatch.style.setProperty("--swatch-color", color);
+ const preview = document.createElement("span");
+ preview.className = "ai-color-swatch-preview";
+ swatch.appendChild(preview);
+ swatch.appendChild(document.createTextNode(color));
+ frag.appendChild(swatch);
+ lastIndex = HEX_COLOR_RE.lastIndex;
+ }
+ if (lastIndex < text.length) {
+ frag.appendChild(document.createTextNode(text.slice(lastIndex)));
+ }
+ textNode.parentNode.replaceChild(frag, textNode);
+ });
+ }
+
function _appendToolIndicator(toolName, toolId) {
// Remove thinking indicator on first content
if (!_hasReceivedContent) {
@@ -899,6 +1270,7 @@ define(function (require, exports, module) {
$tool.find(".ai-tool-label").text(config.label + "...");
$tool.css("--tool-color", config.color);
$tool.attr("data-tool-icon", config.icon);
+ $tool.data("startTime", Date.now());
$messages.append($tool);
_scrollToBottom();
}
@@ -926,8 +1298,46 @@ define(function (require, exports, module) {
// Update label to include summary
$tool.find(".ai-tool-label").text(detail.summary);
- // Add expandable detail if available
- if (detail.lines && detail.lines.length) {
+ // For TodoWrite, render a mini task-list widget and auto-expand
+ if (toolName === "TodoWrite" && toolInput && toolInput.todos) {
+ const $detail = $('
');
+ const $todoList = $('
');
+ toolInput.todos.forEach(function (todo) {
+ let iconClass, statusClass;
+ if (todo.status === "completed") {
+ iconClass = "fa-solid fa-circle-check";
+ statusClass = "completed";
+ } else if (todo.status === "in_progress") {
+ iconClass = "fa-solid fa-spinner fa-spin";
+ statusClass = "in_progress";
+ } else {
+ iconClass = "fa-regular fa-circle";
+ statusClass = "pending";
+ }
+ const $item = $(
+ '' +
+ ' ' +
+ ' ' +
+ '
'
+ );
+ $item.find(".ai-todo-content").text(todo.content);
+ $todoList.append($item);
+ });
+ $detail.append($todoList);
+ $tool.append($detail);
+ $tool.addClass("ai-tool-expanded");
+ $tool.find(".ai-tool-header").on("click", function () {
+ $tool.toggleClass("ai-tool-expanded");
+ }).css("cursor", "pointer");
+ } else if (toolName === "mcp__phoenix-editor__takeScreenshot") {
+ const $detail = $('
');
+ $tool.append($detail);
+ $tool.data("awaitingScreenshot", true);
+ $tool.find(".ai-tool-header").on("click", function () {
+ $tool.toggleClass("ai-tool-expanded");
+ }).css("cursor", "pointer");
+ } else if (detail.lines && detail.lines.length) {
+ // Add expandable detail if available
const $detail = $('
');
detail.lines.forEach(function (line) {
$detail.append($('
').text(line));
@@ -955,6 +1365,14 @@ define(function (require, exports, module) {
clearTimeout(_toolStreamStaleTimer);
clearInterval(_toolStreamRotateTimer);
+ // Stop the elapsed timer and remove the element
+ const elapsedTimer = $tool.data("elapsedTimer");
+ if (elapsedTimer) {
+ clearInterval(elapsedTimer);
+ $tool.removeData("elapsedTimer");
+ }
+ $tool.find(".ai-tool-elapsed").remove();
+
// Delay marking as done so the streaming preview stays visible briefly.
// The ai-tool-done class hides the preview via CSS; deferring it lets the
// browser paint the preview before it disappears.
@@ -1012,8 +1430,105 @@ define(function (require, exports, module) {
summary: input.skill ? StringUtils.format(Strings.AI_CHAT_TOOL_SKILL_NAME, input.skill) : Strings.AI_CHAT_TOOL_SKILL,
lines: input.args ? [input.args] : []
};
- default:
- return { summary: toolName, lines: [] };
+ case "mcp__phoenix-editor__getEditorState":
+ return { summary: Strings.AI_CHAT_TOOL_EDITOR_STATE, lines: [] };
+ case "mcp__phoenix-editor__takeScreenshot": {
+ let screenshotTarget = Strings.AI_CHAT_TOOL_SCREENSHOT_FULL_PAGE;
+ if (input.selector) {
+ if (input.selector.indexOf("live-preview") !== -1 || input.selector.indexOf("panel-live-preview") !== -1) {
+ screenshotTarget = Strings.AI_CHAT_TOOL_SCREENSHOT_LIVE_PREVIEW;
+ } else {
+ screenshotTarget = input.selector;
+ }
+ }
+ return {
+ summary: StringUtils.format(Strings.AI_CHAT_TOOL_SCREENSHOT_OF, screenshotTarget),
+ lines: input.selector ? [input.selector] : []
+ };
+ }
+ case "mcp__phoenix-editor__execJsInLivePreview":
+ return {
+ summary: Strings.AI_CHAT_TOOL_LIVE_PREVIEW_JS,
+ lines: input.code ? input.code.split("\n").slice(0, 20) : []
+ };
+ case "TodoWrite": {
+ const todos = input.todos || [];
+ const completed = todos.filter(function (t) { return t.status === "completed"; }).length;
+ return {
+ summary: StringUtils.format(Strings.AI_CHAT_TOOL_TASKS_SUMMARY, completed, todos.length),
+ lines: []
+ };
+ }
+ case "mcp__phoenix-editor__controlEditor": {
+ // Multi-operation batch format
+ if (input.operations && input.operations.length) {
+ if (input.operations.length === 1) {
+ // Single operation — show its detail
+ const op = input.operations[0];
+ const fn = (op.filePath || "").split("/").pop();
+ let opSummary;
+ switch (op.operation) {
+ case "open":
+ case "openInWorkingSet":
+ opSummary = "Open " + fn;
+ break;
+ case "close":
+ opSummary = "Close " + fn;
+ break;
+ case "setCursorPos":
+ opSummary = "Go to L" + (op.line || "?") + " in " + fn;
+ break;
+ case "setSelection":
+ opSummary = "Select L" + (op.startLine || "?") + "-L" + (op.endLine || "?") + " in " + fn;
+ break;
+ default:
+ opSummary = Strings.AI_CHAT_TOOL_CONTROL_EDITOR;
+ }
+ return { summary: opSummary, lines: [op.filePath || ""] };
+ }
+ // Multiple operations — summarize
+ const count = input.operations.length;
+ const opTypes = {};
+ input.operations.forEach(function (op) {
+ const t = op.operation || "open";
+ opTypes[t] = (opTypes[t] || 0) + 1;
+ });
+ const parts = Object.keys(opTypes).map(function (t) {
+ const label = (t === "open" || t === "openInWorkingSet") ? "Open" :
+ t === "close" ? "Close" :
+ t === "setCursorPos" ? "Navigate" :
+ t === "setSelection" ? "Select" : t;
+ return label + " " + opTypes[t];
+ });
+ return { summary: parts.join(", ") + " files", lines: [] };
+ }
+ // Legacy single-operation format
+ const fileName = (input.filePath || "").split("/").pop();
+ let opSummary;
+ switch (input.operation) {
+ case "open":
+ case "openInWorkingSet":
+ opSummary = "Open " + fileName;
+ break;
+ case "close":
+ opSummary = "Close " + fileName;
+ break;
+ case "setCursorPos":
+ opSummary = "Go to L" + (input.line || "?") + " in " + fileName;
+ break;
+ case "setSelection":
+ opSummary = "Select L" + (input.startLine || "?") + "-L" + (input.endLine || "?") + " in " + fileName;
+ break;
+ default:
+ opSummary = Strings.AI_CHAT_TOOL_CONTROL_EDITOR;
+ }
+ return { summary: opSummary, lines: [input.filePath || ""] };
+ }
+ default: {
+ // Fallback: use TOOL_CONFIG label if available
+ const cfg = TOOL_CONFIG[toolName];
+ return { summary: cfg ? cfg.label : toolName, lines: [] };
+ }
}
}
@@ -1026,6 +1541,13 @@ define(function (require, exports, module) {
function _finishActiveTools() {
$messages.find(".ai-msg-tool:not(.ai-tool-done)").each(function () {
const $prev = $(this);
+ // Clear any running elapsed timer
+ const et = $prev.data("elapsedTimer");
+ if (et) {
+ clearInterval(et);
+ $prev.removeData("elapsedTimer");
+ }
+ $prev.find(".ai-tool-elapsed").remove();
// _updateToolIndicator already ran — let the delayed timeout handle it
if ($prev.find(".ai-tool-icon").length) {
return;
diff --git a/src/core-ai/aiPhoenixConnectors.js b/src/core-ai/aiPhoenixConnectors.js
index 1807fd08c..41a716e50 100644
--- a/src/core-ai/aiPhoenixConnectors.js
+++ b/src/core-ai/aiPhoenixConnectors.js
@@ -32,13 +32,21 @@ define(function (require, exports, module) {
CommandManager = require("command/CommandManager"),
Commands = require("command/Commands"),
MainViewManager = require("view/MainViewManager"),
+ EditorManager = require("editor/EditorManager"),
FileSystem = require("filesystem/FileSystem"),
+ LiveDevProtocol = require("LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol"),
+ LiveDevMain = require("LiveDevelopment/main"),
+ WorkspaceManager = require("view/WorkspaceManager"),
SnapshotStore = require("core-ai/AISnapshotStore"),
+ EventDispatcher = require("utils/EventDispatcher"),
Strings = require("strings");
// filePath → previous content before edit, for undo/snapshot support
const _previousContentMap = {};
+ // Last screenshot base64 data, for displaying in tool indicators
+ let _lastScreenshotBase64 = null;
+
// --- Editor state ---
/**
@@ -74,11 +82,63 @@ define(function (require, exports, module) {
}
}
- return {
+ const result = {
activeFile: activeFilePath,
workingSet: workingSetPaths,
livePreviewFile: livePreviewFile
};
+
+ // Include cursor/selection info from the active editor
+ const editor = EditorManager.getActiveEditor();
+ if (editor) {
+ const doc = editor.document;
+ const totalLines = doc.getLine(doc.getText().split("\n").length - 1) !== undefined
+ ? doc.getText().split("\n").length : 0;
+
+ if (editor.hasSelection()) {
+ const sel = editor.getSelection();
+ let selectedText = editor.getSelectedText();
+ if (selectedText.length > 10000) {
+ selectedText = selectedText.slice(0, 10000) + "...(truncated, use Read tool for full content)";
+ }
+ result.cursorInfo = {
+ hasSelection: true,
+ startLine: sel.start.line + 1,
+ endLine: sel.end.line + 1,
+ selectedText: selectedText,
+ totalLines: totalLines
+ };
+ } else {
+ const cursor = editor.getCursorPos();
+ const lineNum = cursor.line;
+ const lineText = doc.getLine(lineNum) || "";
+ // Include a few surrounding lines for context
+ const contextStart = Math.max(0, lineNum - 2);
+ const contextEnd = Math.min(totalLines - 1, lineNum + 2);
+ const MAX_LINE_LEN = 200;
+ const contextLines = [];
+ for (let i = contextStart; i <= contextEnd; i++) {
+ const prefix = (i === lineNum) ? "> " : " ";
+ let text = doc.getLine(i) || "";
+ if (text.length > MAX_LINE_LEN) {
+ text = text.slice(0, MAX_LINE_LEN) + "...";
+ }
+ contextLines.push(prefix + (i + 1) + ": " + text);
+ }
+ const trimmedLineText = lineText.length > MAX_LINE_LEN
+ ? lineText.slice(0, MAX_LINE_LEN) + "..." : lineText;
+ result.cursorInfo = {
+ hasSelection: false,
+ line: lineNum + 1,
+ column: cursor.ch + 1,
+ lineText: trimmedLineText,
+ context: contextLines.join("\n"),
+ totalLines: totalLines
+ };
+ }
+ }
+
+ return result;
}
// --- Screenshot ---
@@ -104,6 +164,8 @@ define(function (require, exports, module) {
binary += String.fromCharCode.apply(null, chunk);
}
const base64 = btoa(binary);
+ _lastScreenshotBase64 = base64;
+ exports.trigger("screenshotCaptured", base64);
deferred.resolve({ base64: base64 });
})
.catch(function (err) {
@@ -242,10 +304,175 @@ define(function (require, exports, module) {
});
}
+ /**
+ * Get the last captured screenshot as base64 PNG.
+ * @return {string|null}
+ */
+ function getLastScreenshot() {
+ return _lastScreenshotBase64;
+ }
+
+ // --- Live preview JS execution ---
+
+ /**
+ * Execute JavaScript in the live preview iframe.
+ * Reuses the pattern from phoenix-builder-client.js: fast path if connected,
+ * otherwise auto-opens live preview and waits for connection.
+ * @param {Object} params - { code: string }
+ * @return {$.Promise} resolves with { result } or { error }
+ */
+ function execJsInLivePreview(params) {
+ const deferred = new $.Deferred();
+
+ function _evaluate() {
+ LiveDevProtocol.evaluate(params.code)
+ .done(function (evalResult) {
+ deferred.resolve({ result: JSON.stringify(evalResult) });
+ })
+ .fail(function (err) {
+ deferred.resolve({ error: (err && err.message) || String(err) || "evaluate() failed" });
+ });
+ }
+
+ // Fast path: already connected
+ if (LiveDevProtocol.getConnectionIds().length > 0) {
+ _evaluate();
+ return deferred.promise();
+ }
+
+ // Need to ensure live preview is open and connected
+ const panel = WorkspaceManager.getPanelForID("live-preview-panel");
+ if (!panel || !panel.isVisible()) {
+ CommandManager.execute("file.liveFilePreview");
+ } else {
+ LiveDevMain.openLivePreview();
+ }
+
+ // Wait for a live preview connection (up to 30s)
+ const TIMEOUT = 30000;
+ const POLL_INTERVAL = 500;
+ let settled = false;
+ let pollTimer = null;
+
+ function cleanup() {
+ settled = true;
+ if (pollTimer) {
+ clearInterval(pollTimer);
+ pollTimer = null;
+ }
+ LiveDevProtocol.off("ConnectionConnect.aiExecJsLivePreview");
+ }
+
+ const timeoutTimer = setTimeout(function () {
+ if (settled) { return; }
+ cleanup();
+ deferred.resolve({ error: "Timed out waiting for live preview connection (30s)" });
+ }, TIMEOUT);
+
+ function onConnected() {
+ if (settled) { return; }
+ cleanup();
+ clearTimeout(timeoutTimer);
+ _evaluate();
+ }
+
+ LiveDevProtocol.on("ConnectionConnect.aiExecJsLivePreview", onConnected);
+
+ // Safety-net poll in case the event was missed
+ pollTimer = setInterval(function () {
+ if (settled) {
+ clearInterval(pollTimer);
+ return;
+ }
+ if (LiveDevProtocol.getConnectionIds().length > 0) {
+ onConnected();
+ }
+ }, POLL_INTERVAL);
+
+ return deferred.promise();
+ }
+
+ // --- Editor control ---
+
+ /**
+ * Control the editor: open/close files, set cursor, set selection.
+ * Called from the node-side MCP server via execPeer.
+ * @param {Object} params - { operation, filePath, startLine, startCh, endLine, endCh, line, ch }
+ * @return {$.Promise} resolves with { success: true } or { success: false, error: "..." }
+ */
+ function controlEditor(params) {
+ const deferred = new $.Deferred();
+ const vfsPath = SnapshotStore.realToVfsPath(params.filePath);
+
+ switch (params.operation) {
+ case "open":
+ CommandManager.execute(Commands.CMD_OPEN, { fullPath: vfsPath })
+ .done(function () { deferred.resolve({ success: true }); })
+ .fail(function (err) { deferred.resolve({ success: false, error: String(err) }); });
+ break;
+
+ case "close": {
+ const file = FileSystem.getFileForPath(vfsPath);
+ CommandManager.execute(Commands.FILE_CLOSE, { file: file, _forceClose: true })
+ .done(function () { deferred.resolve({ success: true }); })
+ .fail(function (err) { deferred.resolve({ success: false, error: String(err) }); });
+ break;
+ }
+
+ case "openInWorkingSet":
+ CommandManager.execute(Commands.CMD_ADD_TO_WORKINGSET_AND_OPEN, { fullPath: vfsPath })
+ .done(function () { deferred.resolve({ success: true }); })
+ .fail(function (err) { deferred.resolve({ success: false, error: String(err) }); });
+ break;
+
+ case "setSelection":
+ CommandManager.execute(Commands.CMD_OPEN, { fullPath: vfsPath })
+ .done(function () {
+ const editor = EditorManager.getActiveEditor();
+ if (editor) {
+ editor.setSelection(
+ { line: params.startLine - 1, ch: params.startCh - 1 },
+ { line: params.endLine - 1, ch: params.endCh - 1 },
+ true
+ );
+ deferred.resolve({ success: true });
+ } else {
+ deferred.resolve({ success: false, error: "No active editor after opening file" });
+ }
+ })
+ .fail(function (err) { deferred.resolve({ success: false, error: String(err) }); });
+ break;
+
+ case "setCursorPos":
+ CommandManager.execute(Commands.CMD_OPEN, { fullPath: vfsPath })
+ .done(function () {
+ const editor = EditorManager.getActiveEditor();
+ if (editor) {
+ editor.setCursorPos(params.line - 1, params.ch - 1, true);
+ deferred.resolve({ success: true });
+ } else {
+ deferred.resolve({ success: false, error: "No active editor after opening file" });
+ }
+ })
+ .fail(function (err) { deferred.resolve({ success: false, error: String(err) }); });
+ break;
+
+ default:
+ deferred.resolve({ success: false, error: "Unknown operation: " + params.operation });
+ }
+
+ return deferred.promise();
+ }
+
exports.getEditorState = getEditorState;
exports.takeScreenshot = takeScreenshot;
exports.getFileContent = getFileContent;
exports.applyEditToBuffer = applyEditToBuffer;
exports.getPreviousContent = getPreviousContent;
exports.clearPreviousContentMap = clearPreviousContentMap;
+ exports.getLastScreenshot = getLastScreenshot;
+ exports.execJsInLivePreview = execJsInLivePreview;
+ exports.controlEditor = controlEditor;
+
+ EventDispatcher.makeEventDispatcher(exports);
});
diff --git a/src/core-ai/main.js b/src/core-ai/main.js
index 2a5618299..48bab8a91 100644
--- a/src/core-ai/main.js
+++ b/src/core-ai/main.js
@@ -50,6 +50,14 @@ define(function (require, exports, module) {
return PhoenixConnectors.takeScreenshot(params);
};
+ exports.execJsInLivePreview = async function (params) {
+ return PhoenixConnectors.execJsInLivePreview(params);
+ };
+
+ exports.controlEditor = async function (params) {
+ return PhoenixConnectors.controlEditor(params);
+ };
+
AppInit.appReady(function () {
SidebarTabs.addTab("ai", "AI", "fa-solid fa-wand-magic-sparkles", { priority: 200 });
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js
index 65be0c33d..21a76ca79 100644
--- a/src/nls/root/strings.js
+++ b/src/nls/root/strings.js
@@ -1820,6 +1820,15 @@ define({
"AI_CHAT_TOOL_WRITE": "Write",
"AI_CHAT_TOOL_RUN_CMD": "Run command",
"AI_CHAT_TOOL_SKILL": "Skill",
+ "AI_CHAT_TOOL_EDITOR_STATE": "Editor state",
+ "AI_CHAT_TOOL_SCREENSHOT": "Screenshot",
+ "AI_CHAT_TOOL_SCREENSHOT_OF": "Screenshot of {0}",
+ "AI_CHAT_TOOL_SCREENSHOT_LIVE_PREVIEW": "live preview",
+ "AI_CHAT_TOOL_SCREENSHOT_FULL_PAGE": "full page",
+ "AI_CHAT_TOOL_LIVE_PREVIEW_JS": "Live Preview JS",
+ "AI_CHAT_TOOL_CONTROL_EDITOR": "Editor",
+ "AI_CHAT_TOOL_TASKS": "Tasks",
+ "AI_CHAT_TOOL_TASKS_SUMMARY": "{0} of {1} tasks done",
"AI_CHAT_TOOL_SEARCHED": "Searched: {0}",
"AI_CHAT_TOOL_GREP": "Grep: {0}",
"AI_CHAT_TOOL_READ_FILE": "Read {0}",
@@ -1847,6 +1856,9 @@ define({
"AI_CHAT_WORKING": "Working...",
"AI_CHAT_WRITING": "Writing...",
"AI_CHAT_PROCESSING": "Processing...",
+ "AI_CHAT_CONTEXT_SELECTION": "Selection L{0}-L{1}",
+ "AI_CHAT_CONTEXT_CURSOR": "Line {0}",
+ "AI_CHAT_CONTEXT_LIVE_PREVIEW": "Live Preview",
// demo start - Phoenix Code Playground - Interactive Onboarding
"DEMO_SECTION1_TITLE": "Edit in Live Preview",
diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less
index 972c6a744..911846120 100644
--- a/src/styles/Extn-AIChatPanel.less
+++ b/src/styles/Extn-AIChatPanel.less
@@ -28,7 +28,7 @@
overflow: hidden;
background-color: @bc-sidebar-bg;
color: @project-panel-text-1;
- font-size: 12px;
+ font-size: @sidebar-content-font-size;
}
/* ── Header ─────────────────────────────────────────────────────────── */
@@ -42,7 +42,7 @@
.ai-chat-title {
font-weight: 400;
- font-size: 15px;
+ font-size: @label-font-size;
color: @project-panel-text-2;
line-height: 19px;
}
@@ -54,7 +54,7 @@
background: none;
border: none;
color: @project-panel-text-2;
- font-size: 13px;
+ font-size: @menu-item-font-size;
padding: 0 8px;
border-radius: 3px;
cursor: pointer;
@@ -84,7 +84,7 @@
max-width: 100%;
.ai-msg-label {
- font-size: 10px;
+ font-size: @sidebar-xs-font-size;
color: @project-panel-text-2;
margin-bottom: 3px;
font-weight: 600;
@@ -92,7 +92,7 @@
}
.ai-msg-content {
- font-size: 12px;
+ font-size: @sidebar-content-font-size;
line-height: 1.55;
white-space: normal;
word-wrap: break-word;
@@ -152,7 +152,7 @@
background-color: rgba(255, 255, 255, 0.08);
padding: 1px 4px;
border-radius: 3px;
- font-size: 11px;
+ font-size: @sidebar-small-font-size;
font-family: 'SourceCodePro-Medium', 'SourceCodePro', monospace;
}
@@ -168,7 +168,7 @@
background: none;
padding: 0;
border-radius: 0;
- font-size: 11px;
+ font-size: @sidebar-small-font-size;
line-height: 1.5;
color: @project-panel-text-1;
}
@@ -195,29 +195,34 @@
color: @project-panel-text-1;
}
- h1 { font-size: 14px; margin: 12px 0 4px 0; }
- h2 { font-size: 13px; margin: 10px 0 4px 0; }
- h3 { font-size: 12px; margin: 8px 0 3px 0; }
- h4 { font-size: 12px; margin: 6px 0 2px 0; opacity: 0.85; }
+ h1 { font-size: @label-font-size; margin: 12px 0 4px 0; }
+ h2 { font-size: @menu-item-font-size; margin: 10px 0 4px 0; }
+ h3 { font-size: @sidebar-content-font-size; margin: 8px 0 3px 0; }
+ h4 { font-size: @sidebar-content-font-size; margin: 6px 0 2px 0; opacity: 0.85; }
table {
- width: 100%;
+ display: block;
+ overflow-x: auto;
border-collapse: collapse;
margin: 6px 0;
- font-size: 11px;
+ font-size: @sidebar-small-font-size;
+ cursor: default;
}
th, td {
padding: 4px 8px;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
+ max-width: 50%;
+ white-space: nowrap;
+ overflow-wrap: normal;
}
th {
font-weight: 600;
color: @project-panel-text-2;
border-bottom-color: rgba(255, 255, 255, 0.1);
- font-size: 10px;
+ font-size: @sidebar-xs-font-size;
text-transform: uppercase;
letter-spacing: 0.3px;
}
@@ -246,7 +251,7 @@
}
.ai-tool-icon {
- font-size: 10px;
+ font-size: @sidebar-xs-font-size;
width: 14px;
text-align: center;
flex-shrink: 0;
@@ -264,7 +269,7 @@
}
.ai-tool-label {
- font-size: 11px;
+ font-size: @sidebar-small-font-size;
color: @project-panel-text-2;
line-height: 1.3;
@@ -279,8 +284,24 @@
padding: 0 8px 4px 28px;
}
+ .ai-tool-screenshot {
+ max-width: 100%;
+ border-radius: 4px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ margin-top: 4px;
+ max-height: 120px;
+ object-fit: cover;
+ object-position: top left;
+ cursor: pointer;
+ transition: max-height 0.2s ease;
+ }
+
+ &.ai-tool-expanded .ai-tool-screenshot.expanded {
+ max-height: none;
+ }
+
.ai-tool-detail-line {
- font-size: 10px;
+ font-size: @sidebar-xs-font-size;
font-family: 'SourceCodePro-Medium', 'SourceCodePro', monospace;
color: @project-panel-text-2;
opacity: 0.7;
@@ -290,7 +311,7 @@
}
.ai-tool-preview {
- font-size: 10px;
+ font-size: @sidebar-xs-font-size;
font-family: 'SourceCodePro-Medium', 'SourceCodePro', monospace;
color: @project-panel-text-2;
opacity: 0.5;
@@ -336,7 +357,7 @@
.ai-tool-diff-toggle {
background: none;
border: none;
- font-size: 10px;
+ font-size: @sidebar-xs-font-size;
color: @project-panel-text-2;
padding: 1px 4px;
cursor: pointer;
@@ -351,7 +372,7 @@
.ai-tool-diff {
display: none;
padding: 4px 8px 4px 28px;
- font-size: 11px;
+ font-size: @sidebar-small-font-size;
font-family: 'SourceCodePro-Medium', 'SourceCodePro', monospace;
line-height: 1.5;
overflow-x: auto;
@@ -373,10 +394,51 @@
}
.ai-tool-edit-error {
- font-size: 11px;
+ font-size: @sidebar-small-font-size;
color: #e88;
padding: 3px 8px 3px 28px;
}
+
+ .ai-tool-elapsed {
+ font-size: @sidebar-xs-font-size;
+ color: @project-panel-text-2;
+ opacity: 0.5;
+ margin-left: auto;
+ }
+}
+
+/* ── TodoWrite task list widget ────────────────────────────────────── */
+.ai-todo-list {
+ padding: 2px 8px 4px 28px;
+}
+
+.ai-todo-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 6px;
+ padding: 2px 0;
+ font-size: @sidebar-small-font-size;
+}
+
+.ai-todo-icon {
+ width: 14px;
+ text-align: center;
+ flex-shrink: 0;
+ font-size: @sidebar-xs-font-size;
+
+ &.completed { color: #66bb6a; }
+ &.in_progress { color: #e8a838; }
+ &.pending { color: @project-panel-text-2; opacity: 0.4; }
+}
+
+.ai-todo-content {
+ color: @project-panel-text-2;
+ line-height: 1.4;
+
+ &.completed {
+ text-decoration: line-through;
+ opacity: 0.6;
+ }
}
@keyframes ai-spin {
@@ -403,6 +465,25 @@
}
}
+/* ── Inline color swatch ───────────────────────────────────────────── */
+.ai-color-swatch {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ font-family: 'SourceCodePro-Medium', 'SourceCodePro', monospace;
+ font-size: @sidebar-small-font-size;
+}
+
+.ai-color-swatch-preview {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 2px;
+ background-color: var(--swatch-color);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ flex-shrink: 0;
+}
+
@keyframes ai-dot-pulse {
0%, 60%, 100% { opacity: 0.2; transform: scale(0.8); }
30% { opacity: 0.8; transform: scale(1); }
@@ -420,7 +501,7 @@
display: flex;
align-items: center;
justify-content: space-between;
- font-size: 11px;
+ font-size: @sidebar-small-font-size;
font-weight: 600;
color: @project-panel-text-2;
margin-bottom: 4px;
@@ -430,7 +511,7 @@
background: none;
border: 1px solid rgba(200, 160, 80, 0.3);
color: rgba(220, 180, 100, 0.9);
- font-size: 10px;
+ font-size: @sidebar-xs-font-size;
padding: 1px 8px;
border-radius: 3px;
cursor: pointer;
@@ -460,7 +541,7 @@
}
.ai-edit-summary-name {
- font-size: 11px;
+ font-size: @sidebar-small-font-size;
font-family: 'SourceCodePro-Medium', 'SourceCodePro', monospace;
color: @project-panel-text-2;
overflow: hidden;
@@ -478,7 +559,7 @@
.ai-edit-summary-stats {
display: flex;
gap: 6px;
- font-size: 10px;
+ font-size: @sidebar-xs-font-size;
font-family: 'SourceCodePro-Medium', 'SourceCodePro', monospace;
flex-shrink: 0;
}
@@ -504,7 +585,7 @@
background: none;
border: 1px dashed rgba(200, 160, 80, 0.3);
color: rgba(220, 180, 100, 0.7);
- font-size: 10px;
+ font-size: @sidebar-xs-font-size;
padding: 2px 14px;
border-radius: 3px;
cursor: pointer;
@@ -554,7 +635,7 @@
color: #e88;
border-left: 2px solid rgba(255, 80, 80, 0.4);
padding: 4px 8px;
- font-size: 11px;
+ font-size: @sidebar-small-font-size;
background-color: rgba(255, 80, 80, 0.04);
border-radius: 0 3px 3px 0;
}
@@ -566,7 +647,7 @@
align-items: center;
gap: 6px;
padding: 4px 10px;
- font-size: 11px;
+ font-size: @sidebar-small-font-size;
color: @project-panel-text-2;
flex-shrink: 0;
opacity: 0.6;
@@ -609,13 +690,77 @@
}
}
+ .ai-chat-context-bar {
+ display: none;
+ flex-wrap: wrap;
+ gap: 4px;
+ padding: 0 4px 4px 4px;
+
+ &.has-chips {
+ display: flex;
+ }
+ }
+
+ .ai-context-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ font-size: @sidebar-xs-font-size;
+ line-height: 1;
+ padding: 3px 6px;
+ border-radius: 3px;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ color: @project-panel-text-2;
+ background: rgba(255, 255, 255, 0.04);
+ max-width: 100%;
+ overflow: hidden;
+
+ .ai-context-chip-icon {
+ flex-shrink: 0;
+ font-size: 10px;
+ opacity: 0.7;
+ }
+
+ .ai-context-chip-label {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .ai-context-chip-close {
+ flex-shrink: 0;
+ background: none;
+ border: none;
+ color: @project-panel-text-2;
+ font-size: @sidebar-small-font-size;
+ line-height: 1;
+ padding: 0 0 0 2px;
+ cursor: pointer;
+ opacity: 0.5;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
+
+ .ai-context-chip-livepreview {
+ border-color: rgba(76, 175, 80, 0.3);
+ .ai-context-chip-icon { color: #66bb6a; }
+ }
+
+ .ai-context-chip-selection {
+ border-color: rgba(107, 158, 255, 0.3);
+ .ai-context-chip-icon { color: #6b9eff; }
+ }
+
.ai-chat-textarea {
flex: 1;
min-width: 0;
background: none;
border: none !important;
color: @project-panel-text-1;
- font-size: 12px;
+ font-size: @sidebar-content-font-size;
font-family: inherit;
padding: 7px 0 7px 10px;
margin: 0;
@@ -657,7 +802,7 @@
}
i {
- font-size: 12px;
+ font-size: @sidebar-content-font-size;
}
}
@@ -681,7 +826,7 @@
}
i {
- font-size: 12px;
+ font-size: @sidebar-content-font-size;
}
}
}
@@ -704,14 +849,14 @@
}
.ai-unavailable-title {
- font-size: 13px;
+ font-size: @menu-item-font-size;
font-weight: 600;
color: @project-panel-text-1;
margin-bottom: 6px;
}
.ai-unavailable-message {
- font-size: 11px;
+ font-size: @sidebar-small-font-size;
line-height: 1.5;
margin-bottom: 12px;
opacity: 0.6;
@@ -721,7 +866,7 @@
background: none;
border: 1px solid rgba(255, 255, 255, 0.12);
color: @project-panel-text-2;
- font-size: 11px;
+ font-size: @sidebar-small-font-size;
padding: 3px 12px;
border-radius: 3px;
cursor: pointer;
diff --git a/src/styles/brackets_variables.less b/src/styles/brackets_variables.less
index 663c67166..b690782ed 100644
--- a/src/styles/brackets_variables.less
+++ b/src/styles/brackets_variables.less
@@ -33,6 +33,9 @@
@title-font-size: 18px; // headings such as the editor titlebar
@label-font-size: 15px; // labels on buttons, menubar, etc.
@menu-item-font-size: 14px; // individual menu items
+@sidebar-content-font-size: 13px; // body text in sidebar panels (e.g. AI chat)
+@sidebar-small-font-size: 12px; // secondary content, inline code, status text
+@sidebar-xs-font-size: 11px; // meta info, timestamps, chips
/* CSS triangles */
@inline-triangle-size: 9px;