Compare commits

...

3 Commits

Author SHA1 Message Date
f53f35bbed 계획 모드 설정 제거와 플랜 승인 UX 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
메인 설정과 AX Agent 내부 설정에서 계획 모드 UI를 숨기고 저장값을 항상 off로 고정했습니다.

AgentLoop 런타임도 계획 모드를 off로 고정해 코워크와 코드에서 자동 계획 승인 팝업이 반복 노출되지 않도록 정리했습니다.

PlanViewerWindow는 AX Agent 창 owner 리소스를 직접 받아 같은 테마 축을 따르도록 바꾸고 인라인 승인 버튼 중복 노출을 제거했습니다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 오류 0
2026-04-05 14:12:22 +09:00
9fafcd0192 코워크 문서 계획/생성 흐름을 claw-code 기준으로 복구
- document_plan 결과에서 body 골격과 후속 생성 도구를 안정적으로 추출하도록 AgentLoop 분기를 수정

- 코워크 문서형 작업은 planMode=off여도 계획 선행(always) 경로를 타도록 보정

- 코워크 시스템 프롬프트를 강화해 계획만 제시하고 끝나지 않고 실제 문서 파일 생성까지 이어지게 조정

- README와 DEVELOPMENT 문서에 2026-04-05 16:02 (KST) 기준 변경 이력 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 14:05:18 +09:00
35fbfc933d AX Agent: 실행 이벤트 세션 변이 경로 엔진으로 통합
- ChatWindow에 중복돼 있던 실행 이벤트/Agent run 교차 탭 복원 로직을 AxAgentExecutionEngine helper로 이동함

- AppendExecutionEvent, AppendAgentRun이 공통 session mutation 경로를 사용하도록 정리해 이후 runtime state 공통화 기반을 마련함

- README와 DEVELOPMENT 문서에 2026-04-05 15:42 (KST) 기준 변경 이력 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 13:58:16 +09:00
12 changed files with 275 additions and 126 deletions

View File

@@ -7,6 +7,16 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
개발 참고: Claw Code 동등성 작업 추적 문서
`docs/claw-code-parity-plan.md`
- 업데이트: 2026-04-05 16:02 (KST)
- `document_plan` 후속 실행 분기를 `claw-code` 기준으로 다시 보강했습니다. 이제 문서 플래너 출력에서 body 골격과 즉시 실행 지시를 깨진 문자열 비교에 의존하지 않고 안정적으로 추출해, `html_create / document_assemble / docx_create / markdown_create` 후속 호출 유도가 실제로 이어집니다.
- 코워크 문서형 작업은 설정이 `planMode=off`여도 내부적으로 `always` 플랜 경로를 타도록 보정했습니다. 그래서 문서/보고서/제안서 요청은 먼저 계획을 세우고, 그 계획을 바탕으로 실제 문서 생성 단계까지 이어가도록 정리했습니다.
- 코워크 시스템 프롬프트도 강화해 문서 작업은 계획만 제시하고 끝내지 말고 실제 산출물 파일 경로까지 만들어야 완료로 판단하도록 바꿨습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 15:42 (KST)
- AX Agent 엔진 공통화 1차로, Cowork/Code 실행 이벤트와 Agent run 기록을 탭별 현재 대화에 누적한 뒤 원래 활성 탭 대화를 복원하는 로직을 `ChatWindow`에서 `AxAgentExecutionEngine` helper로 옮겼습니다.
- 이제 실행 이벤트/최근 run 기록 반영 시 창 코드가 직접 교차 탭 복원 경로를 중복 처리하지 않고, 엔진의 공통 세션 mutation 경로를 사용합니다.
- 업데이트: 2026-04-05 15:34 (KST)
- AX Agent 개선 계획 기준을 이전 AX 비교본이 아니라 실제 `claw-code` 런타임 축으로 다시 고정했습니다. 현재 참조 spine은 `bootstrap/state.ts -> bridge/initReplBridge.ts -> bridge/sessionRunner.ts -> screens/REPL.tsx -> components/Messages.tsx -> components/StatusLine.tsx` 입니다.
- 이에 맞춰 AX Agent 개선도 `상태 정규화 -> 실행 준비 공통화 -> AgentLoop 이벤트 정규화 -> 타임라인 렌더 일원화 -> 컴포저/상태바 단순화 -> 복구/재개 검증` 순서로 진행하도록 parity 문서를 갱신했습니다.
@@ -796,6 +806,10 @@ ow + toggle 시각 언어로 통일했습니다.
- 이제 `도구` 탭에서는 훅과 도구/커넥터 목록을, `스킬` 탭에서는 스킬 폴더, 슬래시 설정, 드래그 앤 드롭, 로드된 스킬, 폴백 모델, MCP 서버를, `차단` 탭에서는 차단 경로/확장자만 관리합니다. 같이 [SettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml.cs) 에서 메인 설정의 `AX Agent` 바로가기 탭을 좌측 사이드바 맨 아래로 재배치했습니다.
- 런처 하단 바도 요소별로 제어할 수 있게 바꿨습니다. [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs), [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml) 에 `성능 / 포모도로 / 메모 / 날씨 / 일정 / 배터리` 하단 위젯 표시 토글을 추가해서 일반 설정에서 항목별로 바로 켜고 끌 수 있게 했습니다.
- [LauncherWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.xaml), [LauncherWindow.Widgets.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.Widgets.cs) 에서는 `Ollama / API / MCP` 서버 상태 위젯을 런처 하단 기능에서 완전히 제거했고, 남은 위젯들만 설정값에 따라 실제 표시되도록 연결했습니다. 배터리 위젯도 노트북 상태와 사용자 토글을 함께 반영해 보이게 정리했습니다.
- `claw-code` 기준으로 계획 UX도 다시 눌렀습니다. [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs) 에서 저장된 `PlanMode` 값과 무관하게 런타임 계획 모드를 `off`로 고정해, 코워크/코드에서 매번 계획 승인 팝업이 뜨지 않도록 바꿨습니다.
- [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml), [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서는 메인 설정과 AX Agent 내부 설정의 `계획 모드` 행을 숨겼고, [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs) 에서도 항상 `off`만 저장/반영되게 정리했습니다.
- 계획 확인 팝업은 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs), [PlanViewerWindow.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/PlanViewerWindow.cs) 기준으로 AX Agent 창을 owner로 받아 리소스를 그대로 합치게 바꿨고, 채팅 본문에 별도 인라인 승인 버튼을 다시 꽂지 않도록 정리했습니다.
- 업데이트: 2026-04-05 16:20 (KST)
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 15:16 (KST)

View File

@@ -1,7 +1,12 @@
# AX Copilot - 媛쒕컻 臾몄꽌
- Document update: 2026-04-05 16:02 (KST) - Fixed the Cowork document execution handoff around `document_plan`. The loop no longer depends on broken localized marker strings to detect the scaffold/body block or the immediate-next-step hint; it now extracts body markers robustly and resolves the correct follow-up tool (`html_create`, `document_assemble`, `docx_create`, `markdown_create`) before re-prompting the model.
- Document update: 2026-04-05 16:02 (KST) - Added `ResolveEffectivePlanMode(...)` so Cowork document/content tasks automatically use the `always` plan path even when the persisted plan mode is `off`. This brings Cowork closer to the `claw-code` expectation of plan-first execution for document-heavy work.
- Document update: 2026-04-05 16:02 (KST) - Strengthened `BuildCoworkSystemPrompt()` so document/report/proposal/manual requests must produce an execution plan first and are not considered complete until a real output file path has been created or updated.
- Document update: 2026-04-05 15:34 (KST) - Rebased the AX Agent improvement plan on actual `claw-code` runtime files instead of prior AX snapshots. The active reference spine is `src/bootstrap/state.ts -> src/bridge/initReplBridge.ts -> src/bridge/sessionRunner.ts -> src/screens/REPL.tsx -> src/components/Messages.tsx -> src/components/StatusLine.tsx`.
- Document update: 2026-04-05 15:34 (KST) - Locked the AX implementation order to the same quality sequence used by that spine: runtime state canonicalization, prepared execution unification, loop event normalization, timeline render parity, composer/status strip simplification, and recovery/resume validation.
- Document update: 2026-04-05 15:42 (KST) - Moved the cross-tab conversation restoration path for execution events and agent-run history from `ChatWindow.xaml.cs` into `AxAgentExecutionEngine`. `AppendExecutionEvent` and `AppendAgentRun` now go through one engine-owned session mutation helper, which preserves the active tab conversation while updating the target tab timeline.
- Document update: 2026-04-05 15:42 (KST) - Verified the first runtime-state/common-engine step with `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` and confirmed warning 0 / error 0.
- Document update: 2026-04-05 07:11 (KST) - Simplified the AX Agent footer for Cowork/Code by removing the duplicated `MoodIconPanel` chip group from those tabs and leaving workspace context only in the main folder path row. Also removed the outline border from the data-usage button so the footer option strip reads flatter and less pill-heavy.
- Document update: 2026-04-05 07:08 (KST) - Improved AX Agent responsiveness in three hot paths: added an ordered meta cache in `ChatStorageService` so repeated conversation-list refreshes stop re-sorting the full meta set every time, short-circuited `SaveConversationSettings()` when permission/data-usage/mood/output-format values are unchanged, and debounced the sidebar conversation search refresh to avoid re-filtering on every keystroke.
- Document update: 2026-04-05 02:00 (KST) - Reworked the AX Agent in-chat gear overlay navigation itself to match the restored internal settings taxonomy: `basic / chat / cowork / code / dev / tools / skill-block`. The left nav labels now follow that scheme, and the overlay rows/toggles are regrouped per tab instead of the earlier `common / service / permission / advanced` split.
@@ -4565,3 +4570,8 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
- 배터리 위젯은 사용자 토글과 실제 배터리 가용 상태를 함께 반영하도록 `UpdateWidgetVisibility()`에서 최종 가시성을 결정하게 바꿨고, 모든 위젯이 꺼져 있으면 하단 위젯 바 전체도 자동으로 숨깁니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 15:16 (KST)
- `claw-code` 기준 계획 UX 정리도 반영했습니다. [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs) 의 `ResolveEffectivePlanMode(...)` 는 이제 항상 `off`를 반환해, 저장된 예전 계획 모드 값이 남아 있어도 런타임에서는 자동 계획/자동 승인 팝업이 다시 살아나지 않게 했습니다.
- [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml) 과 [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 메인 설정과 AX Agent 내부 설정의 `계획 모드` UI를 `Collapsed`로 전환했고, [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs) 도 `off` 고정으로 바꿔 예전 persisted 값이 다시 UI나 상태바에 반영되지 않게 정리했습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `CreatePlanDecisionCallback()`은 더 이상 채팅 본문에 인라인 승인 버튼을 추가하지 않고, [PlanViewerWindow.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/PlanViewerWindow.cs) 를 AX Agent 창 owner와 merged resources 기준으로 생성해 플랜 팝업이 AX Agent 테마와 같은 리소스 축을 따르도록 바꿨습니다.
- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\`
- 업데이트: 2026-04-05 16:20 (KST)

View File

@@ -200,7 +200,7 @@ public partial class AgentLoopService
maxRetry = ComputeQualityAwareMaxRetry(maxRetry, recentTaskRetryQuality, taskPolicy.TaskType);
// 플랜 모드 설정
var planMode = llm.PlanMode ?? "off"; // off | always | auto
var planMode = ResolveEffectivePlanMode(llm.PlanMode, ActiveTab, taskPolicy.TaskType); // off | always | auto
var context = BuildContext();
InjectTaskTypeGuidance(messages, taskPolicy);
@@ -2096,6 +2096,11 @@ public partial class AgentLoopService
return string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase) ? "feature" : "general";
}
internal static string ResolveEffectivePlanMode(string? configuredPlanMode, string? activeTab, string? taskType)
{
return "off";
}
private static void InjectTaskTypeGuidance(List<ChatMessage> messages, TaskTypePolicy taskPolicy)
{
if (messages.Any(m => m.Role == "user" && m.Content.StartsWith("[System:TaskType]", StringComparison.OrdinalIgnoreCase)))

View File

@@ -1327,31 +1327,82 @@ public partial class AgentLoopService
return;
documentPlanCalled = true;
var po = result.Output;
var po = result.Output ?? string.Empty;
var pm = System.Text.RegularExpressions.Regex.Match(po, @"path:\s*""([^""]+)""");
if (pm.Success) documentPlanPath = pm.Groups[1].Value;
var tm = System.Text.RegularExpressions.Regex.Match(po, @"title:\s*""([^""]+)""");
if (tm.Success) documentPlanTitle = tm.Groups[1].Value;
var bs = po.IndexOf("--- body ?쒖옉 ---", StringComparison.Ordinal);
var be = po.IndexOf("--- body ??---", StringComparison.Ordinal);
if (bs >= 0 && be > bs)
documentPlanScaffold = po[(bs + "--- body ?쒖옉 ---".Length)..be].Trim();
documentPlanScaffold = ExtractDocumentPlanScaffold(po);
if (!result.Output.Contains("利됱떆 ?ㅽ뻾:", StringComparison.Ordinal))
if (!ContainsDocumentPlanFollowUpInstruction(po))
return;
var toolHint = result.Output.Contains("html_create", StringComparison.OrdinalIgnoreCase) ? "html_create" :
result.Output.Contains("document_assemble", StringComparison.OrdinalIgnoreCase) ? "document_assemble" :
result.Output.Contains("file_write", StringComparison.OrdinalIgnoreCase) ? "file_write" : "html_create";
var toolHint = ResolveDocumentPlanFollowUpTool(po);
messages.Add(new ChatMessage
{
Role = "user",
Content = $"document_plan???꾨즺?섏뿀?듬땲?? " +
$"??寃곌낵??body/sections??[?댁슜...] 遺€遺꾩쓣 ?ㅼ젣 ?곸꽭 ?댁슜?쇰줈 紐⑤몢 梨꾩썙??" +
$"{toolHint} ?꾧뎄瑜?吏€湲?利됱떆 ?몄텧?섏꽭?? " +
$"媛??뱀뀡留덈떎 諛섎뱶??異⑸텇???댁슜???묒꽦?섍퀬, ?ㅻ챸 ?놁씠 ?꾧뎄瑜?諛붾줈 ?몄텧?섏꽭??"
Content =
"document_plan이 완료되었습니다. " +
"방금 생성된 골격의 [내용...] 자리와 각 섹션 내용을 실제 상세 본문으로 모두 채운 뒤 " +
$"{toolHint} 도구를 지금 즉시 호출하세요. " +
"설명만 하지 말고 실제 문서 생성 도구 호출로 바로 이어가세요."
});
EmitEvent(AgentEventType.Thinking, "", $"臾몄꽌 媛쒖슂 ?꾩꽦 ??{toolHint} ?몄텧 以?..");
EmitEvent(AgentEventType.Thinking, "", $"문서 개요 완료 · {toolHint} 실행 유도");
}
private static string? ExtractDocumentPlanScaffold(string output)
{
if (string.IsNullOrWhiteSpace(output))
return null;
var markers = new (string Start, string End)[]
{
("--- body 시작 ---", "--- body 끝 ---"),
("--- body start ---", "--- body end ---"),
("<!-- body start marker -->", "<!-- body end marker -->"),
};
foreach (var (startMarker, endMarker) in markers)
{
var start = output.IndexOf(startMarker, StringComparison.OrdinalIgnoreCase);
if (start < 0)
continue;
var contentStart = start + startMarker.Length;
var end = output.IndexOf(endMarker, contentStart, StringComparison.OrdinalIgnoreCase);
if (end <= contentStart)
continue;
var scaffold = output[contentStart..end].Trim();
if (!string.IsNullOrWhiteSpace(scaffold))
return scaffold;
}
return null;
}
private static bool ContainsDocumentPlanFollowUpInstruction(string output)
{
if (string.IsNullOrWhiteSpace(output))
return false;
return output.Contains("즉시 실행", StringComparison.OrdinalIgnoreCase)
|| output.Contains("immediate next step", StringComparison.OrdinalIgnoreCase)
|| output.Contains("call html_create", StringComparison.OrdinalIgnoreCase)
|| output.Contains("call document_assemble", StringComparison.OrdinalIgnoreCase);
}
private static string ResolveDocumentPlanFollowUpTool(string output)
{
if (output.Contains("document_assemble", StringComparison.OrdinalIgnoreCase))
return "document_assemble";
if (output.Contains("docx_create", StringComparison.OrdinalIgnoreCase))
return "docx_create";
if (output.Contains("markdown_create", StringComparison.OrdinalIgnoreCase))
return "markdown_create";
if (output.Contains("file_write", StringComparison.OrdinalIgnoreCase))
return "file_write";
return "html_create";
}
private void ApplyCodeQualityFollowUpTransition(

View File

@@ -16,6 +16,9 @@ public sealed class AxAgentExecutionEngine
ExecutionMode Mode,
IReadOnlyList<string> PromptStack,
List<ChatMessage> Messages);
public sealed record SessionMutationResult(
ChatConversation CurrentConversation,
ChatConversation UpdatedConversation);
public IReadOnlyList<string> BuildPromptStack(
string? conversationSystem,
@@ -159,6 +162,42 @@ public sealed class AxAgentExecutionEngine
return normalized;
}
public SessionMutationResult AppendExecutionEvent(
ChatSessionStateService session,
ChatStorageService storage,
ChatConversation? activeConversation,
string activeTab,
string targetTab,
AgentEvent evt)
{
return ApplyConversationMutation(
session,
storage,
activeConversation,
activeTab,
targetTab,
normalizedTarget => session.AppendExecutionEvent(normalizedTarget, evt, null));
}
public SessionMutationResult AppendAgentRun(
ChatSessionStateService session,
ChatStorageService storage,
ChatConversation? activeConversation,
string activeTab,
string targetTab,
AgentEvent evt,
string status,
string summary)
{
return ApplyConversationMutation(
session,
storage,
activeConversation,
activeTab,
targetTab,
normalizedTarget => session.AppendAgentRun(normalizedTarget, evt, status, summary, null));
}
public string NormalizeAssistantContent(
ChatConversation conversation,
string runTab,
@@ -208,4 +247,86 @@ public sealed class AxAgentExecutionEngine
FileName = source.FileName,
};
}
private static SessionMutationResult ApplyConversationMutation(
ChatSessionStateService session,
ChatStorageService storage,
ChatConversation? activeConversation,
string activeTab,
string targetTab,
Func<string, ChatConversation> mutate)
{
var normalizedTarget = NormalizeTabName(targetTab);
var normalizedActive = NormalizeTabName(activeTab);
ChatConversation updatedConversation;
if (string.Equals(normalizedTarget, normalizedActive, StringComparison.OrdinalIgnoreCase))
{
session.CurrentConversation = updatedConversation = mutate(normalizedTarget);
return new SessionMutationResult(updatedConversation, updatedConversation);
}
var activeSnapshot = activeConversation;
var previousSessionConversation = session.CurrentConversation;
updatedConversation = mutate(normalizedTarget);
if (activeSnapshot != null
&& string.Equals(NormalizeTabName(activeSnapshot.Tab), normalizedActive, StringComparison.OrdinalIgnoreCase))
{
session.CurrentConversation = activeSnapshot;
return new SessionMutationResult(activeSnapshot, updatedConversation);
}
if (previousSessionConversation != null
&& string.Equals(NormalizeTabName(previousSessionConversation.Tab), normalizedActive, StringComparison.OrdinalIgnoreCase))
{
session.CurrentConversation = previousSessionConversation;
return new SessionMutationResult(previousSessionConversation, updatedConversation);
}
var activeId = session.GetConversationId(normalizedActive);
var restoredConversation = string.IsNullOrWhiteSpace(activeId)
? null
: storage.Load(activeId);
if (restoredConversation != null)
{
session.CurrentConversation = restoredConversation;
return new SessionMutationResult(restoredConversation, updatedConversation);
}
var fallbackConversation = session.LoadOrCreateConversation(normalizedActive, storage, GetFallbackSettings());
session.CurrentConversation = fallbackConversation;
return new SessionMutationResult(fallbackConversation, updatedConversation);
}
private static SettingsService GetFallbackSettings()
{
return (System.Windows.Application.Current as App)?.SettingsService
?? new SettingsService();
}
private static string NormalizeTabName(string? tab)
{
var normalized = (tab ?? "").Trim();
if (string.IsNullOrEmpty(normalized))
return "Chat";
if (normalized.Contains("코워크", StringComparison.OrdinalIgnoreCase))
return "Cowork";
var canonical = new string(normalized
.Where(char.IsLetterOrDigit)
.ToArray())
.ToLowerInvariant();
if (canonical is "cowork" or "coworkcode" or "coworkcodetab")
return "Cowork";
if (normalized.Contains("코드", StringComparison.OrdinalIgnoreCase)
|| canonical is "code" or "codetab")
return "Code";
return "Chat";
}
}

View File

@@ -155,7 +155,9 @@ public class DocumentPlannerTool : IAgentTool
output.AppendLine("(주석의 '활용 가능 요소'는 참고용이며, 내용에 맞게 다른 요소를 써도 됩니다)");
output.AppendLine();
output.AppendLine("--- body 시작 ---");
output.AppendLine("<!-- body start marker -->");
output.Append(bodySb);
output.AppendLine("<!-- body end marker -->");
output.AppendLine("--- body 끝 ---");
output.AppendLine();
output.AppendLine("⚠ html_create를 지금 즉시 호출하세요. 모든 섹션에 충분한 실제 내용을 작성하세요.");

View File

@@ -196,7 +196,7 @@ public sealed class AppStateService
Permissions.FilePermission = PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission);
Permissions.AgentDecisionLevel = llm.AgentDecisionLevel ?? "detailed";
Permissions.PlanMode = llm.PlanMode ?? "off";
Permissions.PlanMode = "off";
Permissions.ToolOverrideCount = llm.ToolPermissions?.Count ?? 0;
Permissions.ToolOverrides = llm.ToolPermissions?
.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)

View File

@@ -370,11 +370,13 @@ public class SettingsViewModel : INotifyPropertyChanged
set { _agentDecisionLevel = value; OnPropertyChanged(); }
}
private string _planMode = "off";
public string PlanMode
{
get => _planMode;
set { _planMode = value; OnPropertyChanged(); }
get => "off";
set
{
OnPropertyChanged();
}
}
private bool _enableMultiPassDocument;
@@ -1138,7 +1140,6 @@ public class SettingsViewModel : INotifyPropertyChanged
_planDiffSeverityMediumRatioPercent = llm.PlanDiffSeverityMediumRatioPercent > 0 ? Math.Clamp(llm.PlanDiffSeverityMediumRatioPercent, 1, 100) : 25;
_planDiffSeverityHighRatioPercent = llm.PlanDiffSeverityHighRatioPercent > 0 ? Math.Clamp(llm.PlanDiffSeverityHighRatioPercent, 1, 100) : 60;
_agentDecisionLevel = llm.AgentDecisionLevel;
_planMode = string.IsNullOrEmpty(llm.PlanMode) ? "off" : llm.PlanMode;
_enableMultiPassDocument = llm.EnableMultiPassDocument;
_enableCoworkVerification = llm.EnableCoworkVerification;
_enableFilePathHighlight = llm.EnableFilePathHighlight;
@@ -1580,7 +1581,7 @@ public class SettingsViewModel : INotifyPropertyChanged
s.Llm.PlanDiffSeverityMediumRatioPercent = _planDiffSeverityMediumRatioPercent;
s.Llm.PlanDiffSeverityHighRatioPercent = _planDiffSeverityHighRatioPercent;
s.Llm.AgentDecisionLevel = _agentDecisionLevel;
s.Llm.PlanMode = _planMode;
s.Llm.PlanMode = "off";
s.Llm.EnableMultiPassDocument = _enableMultiPassDocument;
s.Llm.EnableCoworkVerification = _enableCoworkVerification;
s.Llm.EnableFilePathHighlight = _enableFilePathHighlight;

View File

@@ -2880,7 +2880,7 @@
<ComboBoxItem Content="계획 · 항상 계획" Tag="always"/>
</ComboBox>
</Grid>
<Grid>
<Grid Visibility="Collapsed">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>

View File

@@ -8735,7 +8735,9 @@ public partial class ChatWindow : Window
sb.AppendLine("You are AX Copilot Agent. You can read, write, and edit files using the provided tools.");
sb.AppendLine($"Today's date: {DateTime.Now:yyyy년 M월 d일} ({DateTime.Now:yyyy-MM-dd}, {DateTime.Now:dddd}).");
sb.AppendLine("Available skills: excel_create (.xlsx), docx_create (.docx), csv_create (.csv), markdown_create (.md), html_create (.html), script_create (.bat/.ps1), document_review (품질 검증), format_convert (포맷 변환).");
sb.AppendLine("Always explain your plan step by step BEFORE executing tools. After creating files, summarize what was created.");
sb.AppendLine("Only present a step-by-step execution plan when the user explicitly asks for a plan or when the session is already in plan mode.");
sb.AppendLine("For ordinary Cowork requests, proceed directly with the work instead of stopping for plan approval.");
sb.AppendLine("After creating files, summarize what was created and include the actual output path.");
sb.AppendLine("Do not stop after a single step. Continue autonomously until the request is completed or a concrete blocker (permission denial, missing dependency, hard error) is encountered.");
sb.AppendLine("When adapting external references, rewrite names/structure/comments to AX Copilot style. Avoid clone-like outputs.");
sb.AppendLine("IMPORTANT: When creating documents with dates, always use today's actual date above. Never use placeholder or fictional dates.");
@@ -8746,6 +8748,7 @@ public partial class ChatWindow : Window
sb.AppendLine(" 3. Then immediately call html_create (or docx_create/file_write) using the scaffold from document_plan.");
sb.AppendLine(" 4. Write actual detailed content for EVERY section — no skipping, no placeholders, no minimal content.");
sb.AppendLine(" 5. Do NOT call html_create directly without document_plan for multi-section documents.");
sb.AppendLine(" 6. Do not finish with a plan only. The task is complete only after the document file has actually been created or updated.");
// 문서 품질 검증 루프
sb.AppendLine("\n## Document Quality Review");
@@ -9166,48 +9169,17 @@ public partial class ChatWindow : Window
if (session == null)
return;
var normalizedTarget = NormalizeTabName(targetTab);
var normalizedActive = NormalizeTabName(_activeTab);
ChatConversation updatedConversation;
if (string.Equals(normalizedTarget, normalizedActive, StringComparison.OrdinalIgnoreCase))
{
_currentConversation = updatedConversation = session.AppendAgentRun(normalizedTarget, evt, status, summary, null);
ScheduleConversationPersist(updatedConversation);
return;
}
var activeSnapshot = _currentConversation;
var previousSessionConversation = session.CurrentConversation;
updatedConversation = session.AppendAgentRun(normalizedTarget, evt, status, summary, null);
ScheduleConversationPersist(updatedConversation);
if (activeSnapshot != null && string.Equals(NormalizeTabName(activeSnapshot.Tab), normalizedActive, StringComparison.OrdinalIgnoreCase))
{
session.CurrentConversation = activeSnapshot;
_currentConversation = activeSnapshot;
}
else if (previousSessionConversation != null
&& string.Equals(NormalizeTabName(previousSessionConversation.Tab), normalizedActive, StringComparison.OrdinalIgnoreCase))
{
session.CurrentConversation = previousSessionConversation;
_currentConversation = previousSessionConversation;
}
else
{
var activeId = session.GetConversationId(normalizedActive);
var activeConv = string.IsNullOrWhiteSpace(activeId) ? null : _storage.Load(activeId);
if (activeConv != null)
{
session.CurrentConversation = activeConv;
_currentConversation = activeConv;
}
else
{
var fallback = session.LoadOrCreateConversation(normalizedActive, _storage, _settings);
session.CurrentConversation = fallback;
_currentConversation = fallback;
}
}
var result = _chatEngine.AppendAgentRun(
session,
_storage,
_currentConversation,
_activeTab,
targetTab,
evt,
status,
summary);
_currentConversation = result.CurrentConversation;
ScheduleConversationPersist(result.UpdatedConversation);
}
}
@@ -9219,48 +9191,15 @@ public partial class ChatWindow : Window
if (session == null)
return;
var normalizedTarget = NormalizeTabName(targetTab);
var normalizedActive = NormalizeTabName(_activeTab);
ChatConversation updatedConversation;
if (string.Equals(normalizedTarget, normalizedActive, StringComparison.OrdinalIgnoreCase))
{
_currentConversation = updatedConversation = session.AppendExecutionEvent(normalizedTarget, evt, null);
ScheduleConversationPersist(updatedConversation);
return;
}
var activeSnapshot = _currentConversation;
var previousSessionConversation = session.CurrentConversation;
updatedConversation = session.AppendExecutionEvent(normalizedTarget, evt, null);
ScheduleConversationPersist(updatedConversation);
if (activeSnapshot != null && string.Equals(NormalizeTabName(activeSnapshot.Tab), normalizedActive, StringComparison.OrdinalIgnoreCase))
{
session.CurrentConversation = activeSnapshot;
_currentConversation = activeSnapshot;
}
else if (previousSessionConversation != null
&& string.Equals(NormalizeTabName(previousSessionConversation.Tab), normalizedActive, StringComparison.OrdinalIgnoreCase))
{
session.CurrentConversation = previousSessionConversation;
_currentConversation = previousSessionConversation;
}
else
{
var activeId = session.GetConversationId(normalizedActive);
var activeConv = string.IsNullOrWhiteSpace(activeId) ? null : _storage.Load(activeId);
if (activeConv != null)
{
session.CurrentConversation = activeConv;
_currentConversation = activeConv;
}
else
{
var fallback = session.LoadOrCreateConversation(normalizedActive, _storage, _settings);
session.CurrentConversation = fallback;
_currentConversation = fallback;
}
}
var result = _chatEngine.AppendExecutionEvent(
session,
_storage,
_currentConversation,
_activeTab,
targetTab,
evt);
_currentConversation = result.CurrentConversation;
ScheduleConversationPersist(result.UpdatedConversation);
}
}
@@ -9920,7 +9859,7 @@ public partial class ChatWindow : Window
// PlanViewerWindow 생성 또는 재사용
if (_planViewerWindow == null || !IsWindowAlive(_planViewerWindow))
{
_planViewerWindow = new PlanViewerWindow();
_planViewerWindow = new PlanViewerWindow(this);
_planViewerWindow.Closing += (_, e) =>
{
e.Cancel = true;
@@ -9931,9 +9870,6 @@ public partial class ChatWindow : Window
// 계획 표시 + 승인 대기
_planViewerWindow.ShowPlanAsync(planSummary, steps, tcs);
// 채팅 창에 간략 배너 추가 + 인라인 승인 버튼도 표시
AddDecisionButtons(tcs, options);
// 하단 바 계획 버튼 표시
ShowPlanButton(true);
});
@@ -14153,7 +14089,7 @@ public partial class ChatWindow : Window
BtnInlineFastMode.Content = GetQuickActionLabel("Fast", llm.FreeTierMode ? "켜짐" : "꺼짐");
BtnInlineReasoning.Content = GetQuickActionLabel("추론", ReasoningLabel(llm.AgentDecisionLevel));
BtnInlinePlanMode.Content = GetQuickActionLabel("계획", PlanModeLabel(llm.PlanMode));
BtnInlinePlanMode.Content = GetQuickActionLabel("계획", PlanModeLabel("off"));
BtnInlinePermission.Content = GetQuickActionLabel("권한", PermissionModeCatalog.ToDisplayLabel(llm.FilePermission));
BtnInlineSkill.Content = $"스킬 · {(llm.EnableSkillSystem ? "On" : "Off")}";
BtnInlineCommandBrowser.Content = "명령/스킬 브라우저";
@@ -14164,7 +14100,7 @@ public partial class ChatWindow : Window
ApplyQuickActionVisual(BtnInlineFastMode, llm.FreeTierMode, "#ECFDF5", "#166534");
ApplyQuickActionVisual(BtnInlineReasoning, !string.Equals(llm.AgentDecisionLevel, "normal", StringComparison.OrdinalIgnoreCase), "#EEF2FF", "#1D4ED8");
ApplyQuickActionVisual(BtnInlinePlanMode, !string.Equals(llm.PlanMode, "off", StringComparison.OrdinalIgnoreCase), "#EEF2FF", "#4338CA");
ApplyQuickActionVisual(BtnInlinePlanMode, false, "#EEF2FF", "#4338CA");
ApplyQuickActionVisual(BtnInlinePermission,
!string.Equals(PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission), PermissionModeCatalog.Deny, StringComparison.OrdinalIgnoreCase),
"#FFF7ED",
@@ -16399,7 +16335,7 @@ public partial class ChatWindow : Window
SelectComboTag(CmbOverlayOperationMode, OperationModePolicy.Normalize(_settings.Settings.OperationMode));
SelectComboTag(CmbOverlayFolderDataUsage, _folderDataUsage);
SelectComboTag(CmbOverlayPermission, PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission));
SelectComboTag(CmbOverlayPlanMode, llm.PlanMode);
SelectComboTag(CmbOverlayPlanMode, "off");
SelectComboTag(CmbOverlayReasoning, llm.AgentDecisionLevel);
SelectComboTag(CmbOverlayFastMode, llm.FreeTierMode ? "on" : "off");
SelectComboTag(CmbOverlayDefaultOutputFormat, llm.DefaultOutputFormat ?? "auto");
@@ -16897,10 +16833,10 @@ public partial class ChatWindow : Window
private void CmbOverlayPlanMode_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isOverlaySettingsSyncing || CmbOverlayPlanMode.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
if (_isOverlaySettingsSyncing)
return;
_settings.Settings.Llm.PlanMode = tag;
_settings.Settings.Llm.PlanMode = "off";
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
}
@@ -17113,7 +17049,7 @@ public partial class ChatWindow : Window
private void BtnInlinePlanMode_Click(object sender, RoutedEventArgs e)
{
var llm = _settings.Settings.Llm;
llm.PlanMode = NextPlanMode(llm.PlanMode);
llm.PlanMode = "off";
_settings.Save();
_appState.LoadFromSettings(_settings);
RefreshInlineSettingsPanel();

View File

@@ -49,28 +49,37 @@ internal sealed class PlanViewerWindow : Window
private bool _isExecuting;
private readonly string _uiExpressionLevel;
public PlanViewerWindow()
public PlanViewerWindow(Window? owner = null)
{
if (owner != null)
{
Owner = owner;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
Resources.MergedDictionaries.Add(owner.Resources);
}
_uiExpressionLevel = ResolveUiExpressionLevel();
Width = 640;
Height = 520;
MinWidth = 480;
MinHeight = 360;
WindowStartupLocation = WindowStartupLocation.CenterScreen;
WindowStartupLocation = owner == null
? WindowStartupLocation.CenterScreen
: WindowStartupLocation.CenterOwner;
WindowStyle = WindowStyle.None;
AllowsTransparency = true;
Background = Brushes.Transparent;
ResizeMode = ResizeMode.CanResize; // WndProc로 직접 처리
ShowInTaskbar = false;
var bgBrush = Application.Current.TryFindResource("LauncherBackground") as Brush
var bgBrush = TryFindResource("LauncherBackground") as Brush
?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
var primaryText = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var accentBrush = TryFindResource("AccentColor") as Brush
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
var borderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var root = new Border
{
@@ -151,7 +160,7 @@ internal sealed class PlanViewerWindow : Window
mainGrid.Children.Add(_statusBar);
// ── 툴바: 모두 열기 / 모두 닫기 ──
var hoverBgTb = Application.Current.TryFindResource("ItemHoverBackground") as Brush
var hoverBgTb = TryFindResource("ItemHoverBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
var toolBar = new StackPanel
{

View File

@@ -4653,7 +4653,7 @@
</StackPanel>
</Grid>
</Border>
<Border Style="{StaticResource AgentSettingsRow}">
<Border Style="{StaticResource AgentSettingsRow}" Visibility="Collapsed">
<Grid>
<StackPanel HorizontalAlignment="Left" Margin="0,0,180,0">
<StackPanel Orientation="Horizontal">