Skip to content

feat(settings): add a Projects page with a default project - #2446

Merged
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page
Aug 7, 2026
Merged

feat(settings): add a Projects page with a default project#2446
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page

Conversation

@jackwener

Copy link
Copy Markdown
Member

改了什么

设置 · 偏好 新增「项目」页:项目列表管理 + 默认项目。task #160

为什么

项目管理原本散在各处——添加在 composer、重命名在侧栏行菜单,而「新对话从哪个项目打开」根本不是一个设置:它是「上次用过的那个」,由 project-root-controller 隐式决定,UI 上完全看不见也改不了。

三层规则(按优先级)

  1. composer 里显式选了项目 → 该次对话用它(每次对话都能临时覆盖
  2. 否则用设置里的默认项目
  3. 否则维持原来的 last-used 行为(没设默认的用户行为零变化

「默认覆盖 last-used」而不是「仅在没有 last-used 时兜底」:几乎总是有 last-used,兜底式默认等于永远不生效,那就又是一个说话不算话的旋钮。

两个设计决定

存 projectId 不存路径。 catalog 持有 identity,项目 relink 到新位置后 id 不变;存路径会让这个偏好在目录移动后静默指向别处。

不可用项目不能设为默认(按钮禁用,且禁用态自述原因)。#158「任何连接都能设默认」不同:连接失败是单次使用时才暴露、用户可以先设后修;而默认项目指向一个打不开的目录 = 每个新对话都坏,且修复手段(恢复目录)不在 app 内。预防优于降级。

默认项目失效(归档/目录没了)时,回退到 last-used 而不是让建会话失败,并且在设置页把降级说出来——静默降级和假控件是同一类谎。

范围说明

重命名在访达中打开 这一版没进行尾 … 菜单:前者需要仓库里尚不存在的 inline-edit 原语,后者需要新 IPC(app:openPath 只认「当前项目」)。两个能力目前都还在侧栏行菜单里,没有任何能力丢失,作为后续小任务补。

怎么验证的

  • 新增 4 个 main 侧解析测试:默认覆盖 last-used、显式选择覆盖默认、未设默认行为不变、默认失效时回退不炸。故障注入:把默认读取拆掉 → 恰好 1 个测试失败 → 装回全绿。
  • 新增 e2esettings-projects.spec.ts + settings-projects fixture(三个 catalog 条目:可用 / 可用长路径 / 目录真的不存在):列表渲染、不可用行显示「目录不可用」、禁用按钮的 aria-describedby 确实说明原因、设为默认后 Badge 互换且落盘、同时只有一个默认。
  • 新增 3 个 core 归一化测试:未设即无偏好、已设存活、空白 id 丢弃。
  • typecheck / lint / format / build / knip(两个 workspace)全绿;desktop 1751、core 812 全过。
  • 全量测试的既有失败 3 例(maka run process contract ×2、node:sqlite ×1)已确认为存量;runtime-host 那例是 Timing-sensitive tests fail under parallel workspace runner (concurrency=3) #2132 的负载下时序 flake,单跑 725/0 通过。

视觉

预览截图已过审(列表态 / 设为默认态)。截图过程中修掉三处:页头与节头「项目」标题重复、fixture 用编造路径导致整列渲染成不可用态、以及一个匹配不到 Astryx Item 的选择器。

Project management was spread across whichever surface happened to need it —
adding lived in the composer, renaming in the sidebar row menu — and "which
project does a new conversation open in" was not a setting at all. It was
whichever project you used last, decided implicitly by
`project-root-controller` and invisible from the UI.
Settings · 偏好 · 项目 makes the catalog a first-class surface and gives the
default a home. Three layers, in order: an explicit pick in the composer wins
for that conversation; otherwise the configured default; otherwise the
previous last-used behaviour, unchanged for anyone who sets nothing.
The default is stored as a project id rather than a path, because the catalog
owns identity — a relinked project keeps its id, so a stored path would
silently point somewhere else after a move. A default that no longer resolves
(archived, or the folder is gone) falls back to last-used rather than failing
session creation, and the page says so instead of degrading silently.
Unavailable projects cannot be made the default at all: unlike a model
connection, where a failure shows up per use and the user can fix it after
choosing, a default pointing at a missing folder breaks every new conversation
and the repair is outside the app. The disabled control states that reason
itself.
Rename and reveal-in-folder are not in the row menu yet — one needs an
inline-edit primitive this tree does not have, the other an IPC that can open
a path other than the current project's. Both still work from the sidebar row
menu, so no capability is lost.
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:项目设置页全案落地——预览阶段过审的结构(页头/描述行三层规则/行式方言/不可用态)+ 解析逻辑插在 resolveDesktopSessionSelection 的天然三层接缝(composer 单次覆盖免额外判断);「默认胜 last-used」测试带存在理由注释;降级可见且不把账算到新建对话头上;禁用态自述补齐并以 aria-describedby 实证(title 空断言的空过教训留档);单一默认的移动语义有 e2e 钉住。重命名/访达打开按批准降级为后续任务。CI 12 项全绿零失败。合入。

@jackwener
jackwener merged commit ea36be4 into mainAug 7, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(settings): add a Projects page with a default project by jackwener · Pull Request #2446 · apache/maka · GitHub
Skip to content

feat(settings): add a Projects page with a default project - #2446

Merged
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page
Aug 7, 2026
Merged

feat(settings): add a Projects page with a default project#2446
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page

Conversation

@jackwener

Copy link
Copy Markdown
Member

改了什么

设置 · 偏好 新增「项目」页:项目列表管理 + 默认项目。task #160

为什么

项目管理原本散在各处——添加在 composer、重命名在侧栏行菜单,而「新对话从哪个项目打开」根本不是一个设置:它是「上次用过的那个」,由 project-root-controller 隐式决定,UI 上完全看不见也改不了。

三层规则(按优先级)

  1. composer 里显式选了项目 → 该次对话用它(每次对话都能临时覆盖
  2. 否则用设置里的默认项目
  3. 否则维持原来的 last-used 行为(没设默认的用户行为零变化

「默认覆盖 last-used」而不是「仅在没有 last-used 时兜底」:几乎总是有 last-used,兜底式默认等于永远不生效,那就又是一个说话不算话的旋钮。

两个设计决定

存 projectId 不存路径。 catalog 持有 identity,项目 relink 到新位置后 id 不变;存路径会让这个偏好在目录移动后静默指向别处。

不可用项目不能设为默认(按钮禁用,且禁用态自述原因)。#158「任何连接都能设默认」不同:连接失败是单次使用时才暴露、用户可以先设后修;而默认项目指向一个打不开的目录 = 每个新对话都坏,且修复手段(恢复目录)不在 app 内。预防优于降级。

默认项目失效(归档/目录没了)时,回退到 last-used 而不是让建会话失败,并且在设置页把降级说出来——静默降级和假控件是同一类谎。

范围说明

重命名在访达中打开 这一版没进行尾 … 菜单:前者需要仓库里尚不存在的 inline-edit 原语,后者需要新 IPC(app:openPath 只认「当前项目」)。两个能力目前都还在侧栏行菜单里,没有任何能力丢失,作为后续小任务补。

怎么验证的

  • 新增 4 个 main 侧解析测试:默认覆盖 last-used、显式选择覆盖默认、未设默认行为不变、默认失效时回退不炸。故障注入:把默认读取拆掉 → 恰好 1 个测试失败 → 装回全绿。
  • 新增 e2esettings-projects.spec.ts + settings-projects fixture(三个 catalog 条目:可用 / 可用长路径 / 目录真的不存在):列表渲染、不可用行显示「目录不可用」、禁用按钮的 aria-describedby 确实说明原因、设为默认后 Badge 互换且落盘、同时只有一个默认。
  • 新增 3 个 core 归一化测试:未设即无偏好、已设存活、空白 id 丢弃。
  • typecheck / lint / format / build / knip(两个 workspace)全绿;desktop 1751、core 812 全过。
  • 全量测试的既有失败 3 例(maka run process contract ×2、node:sqlite ×1)已确认为存量;runtime-host 那例是 Timing-sensitive tests fail under parallel workspace runner (concurrency=3) #2132 的负载下时序 flake,单跑 725/0 通过。

视觉

预览截图已过审(列表态 / 设为默认态)。截图过程中修掉三处:页头与节头「项目」标题重复、fixture 用编造路径导致整列渲染成不可用态、以及一个匹配不到 Astryx Item 的选择器。

Project management was spread across whichever surface happened to need it —
adding lived in the composer, renaming in the sidebar row menu — and "which
project does a new conversation open in" was not a setting at all. It was
whichever project you used last, decided implicitly by
`project-root-controller` and invisible from the UI.
Settings · 偏好 · 项目 makes the catalog a first-class surface and gives the
default a home. Three layers, in order: an explicit pick in the composer wins
for that conversation; otherwise the configured default; otherwise the
previous last-used behaviour, unchanged for anyone who sets nothing.
The default is stored as a project id rather than a path, because the catalog
owns identity — a relinked project keeps its id, so a stored path would
silently point somewhere else after a move. A default that no longer resolves
(archived, or the folder is gone) falls back to last-used rather than failing
session creation, and the page says so instead of degrading silently.
Unavailable projects cannot be made the default at all: unlike a model
connection, where a failure shows up per use and the user can fix it after
choosing, a default pointing at a missing folder breaks every new conversation
and the repair is outside the app. The disabled control states that reason
itself.
Rename and reveal-in-folder are not in the row menu yet — one needs an
inline-edit primitive this tree does not have, the other an IPC that can open
a path other than the current project's. Both still work from the sidebar row
menu, so no capability is lost.
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:项目设置页全案落地——预览阶段过审的结构(页头/描述行三层规则/行式方言/不可用态)+ 解析逻辑插在 resolveDesktopSessionSelection 的天然三层接缝(composer 单次覆盖免额外判断);「默认胜 last-used」测试带存在理由注释;降级可见且不把账算到新建对话头上;禁用态自述补齐并以 aria-describedby 实证(title 空断言的空过教训留档);单一默认的移动语义有 e2e 钉住。重命名/访达打开按批准降级为后续任务。CI 12 项全绿零失败。合入。

@jackwener
jackwener merged commit ea36be4 into mainAug 7, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(settings): add a Projects page with a default project by jackwener · Pull Request #2446 · apache/maka · GitHub
Skip to content

feat(settings): add a Projects page with a default project - #2446

Merged
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page
Aug 7, 2026
Merged

feat(settings): add a Projects page with a default project#2446
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page

Conversation

@jackwener

Copy link
Copy Markdown
Member

改了什么

设置 · 偏好 新增「项目」页:项目列表管理 + 默认项目。task #160

为什么

项目管理原本散在各处——添加在 composer、重命名在侧栏行菜单,而「新对话从哪个项目打开」根本不是一个设置:它是「上次用过的那个」,由 project-root-controller 隐式决定,UI 上完全看不见也改不了。

三层规则(按优先级)

  1. composer 里显式选了项目 → 该次对话用它(每次对话都能临时覆盖
  2. 否则用设置里的默认项目
  3. 否则维持原来的 last-used 行为(没设默认的用户行为零变化

「默认覆盖 last-used」而不是「仅在没有 last-used 时兜底」:几乎总是有 last-used,兜底式默认等于永远不生效,那就又是一个说话不算话的旋钮。

两个设计决定

存 projectId 不存路径。 catalog 持有 identity,项目 relink 到新位置后 id 不变;存路径会让这个偏好在目录移动后静默指向别处。

不可用项目不能设为默认(按钮禁用,且禁用态自述原因)。#158「任何连接都能设默认」不同:连接失败是单次使用时才暴露、用户可以先设后修;而默认项目指向一个打不开的目录 = 每个新对话都坏,且修复手段(恢复目录)不在 app 内。预防优于降级。

默认项目失效(归档/目录没了)时,回退到 last-used 而不是让建会话失败,并且在设置页把降级说出来——静默降级和假控件是同一类谎。

范围说明

重命名在访达中打开 这一版没进行尾 … 菜单:前者需要仓库里尚不存在的 inline-edit 原语,后者需要新 IPC(app:openPath 只认「当前项目」)。两个能力目前都还在侧栏行菜单里,没有任何能力丢失,作为后续小任务补。

怎么验证的

  • 新增 4 个 main 侧解析测试:默认覆盖 last-used、显式选择覆盖默认、未设默认行为不变、默认失效时回退不炸。故障注入:把默认读取拆掉 → 恰好 1 个测试失败 → 装回全绿。
  • 新增 e2esettings-projects.spec.ts + settings-projects fixture(三个 catalog 条目:可用 / 可用长路径 / 目录真的不存在):列表渲染、不可用行显示「目录不可用」、禁用按钮的 aria-describedby 确实说明原因、设为默认后 Badge 互换且落盘、同时只有一个默认。
  • 新增 3 个 core 归一化测试:未设即无偏好、已设存活、空白 id 丢弃。
  • typecheck / lint / format / build / knip(两个 workspace)全绿;desktop 1751、core 812 全过。
  • 全量测试的既有失败 3 例(maka run process contract ×2、node:sqlite ×1)已确认为存量;runtime-host 那例是 Timing-sensitive tests fail under parallel workspace runner (concurrency=3) #2132 的负载下时序 flake,单跑 725/0 通过。

视觉

预览截图已过审(列表态 / 设为默认态)。截图过程中修掉三处:页头与节头「项目」标题重复、fixture 用编造路径导致整列渲染成不可用态、以及一个匹配不到 Astryx Item 的选择器。

Project management was spread across whichever surface happened to need it —
adding lived in the composer, renaming in the sidebar row menu — and "which
project does a new conversation open in" was not a setting at all. It was
whichever project you used last, decided implicitly by
`project-root-controller` and invisible from the UI.
Settings · 偏好 · 项目 makes the catalog a first-class surface and gives the
default a home. Three layers, in order: an explicit pick in the composer wins
for that conversation; otherwise the configured default; otherwise the
previous last-used behaviour, unchanged for anyone who sets nothing.
The default is stored as a project id rather than a path, because the catalog
owns identity — a relinked project keeps its id, so a stored path would
silently point somewhere else after a move. A default that no longer resolves
(archived, or the folder is gone) falls back to last-used rather than failing
session creation, and the page says so instead of degrading silently.
Unavailable projects cannot be made the default at all: unlike a model
connection, where a failure shows up per use and the user can fix it after
choosing, a default pointing at a missing folder breaks every new conversation
and the repair is outside the app. The disabled control states that reason
itself.
Rename and reveal-in-folder are not in the row menu yet — one needs an
inline-edit primitive this tree does not have, the other an IPC that can open
a path other than the current project's. Both still work from the sidebar row
menu, so no capability is lost.
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:项目设置页全案落地——预览阶段过审的结构(页头/描述行三层规则/行式方言/不可用态)+ 解析逻辑插在 resolveDesktopSessionSelection 的天然三层接缝(composer 单次覆盖免额外判断);「默认胜 last-used」测试带存在理由注释;降级可见且不把账算到新建对话头上;禁用态自述补齐并以 aria-describedby 实证(title 空断言的空过教训留档);单一默认的移动语义有 e2e 钉住。重命名/访达打开按批准降级为后续任务。CI 12 项全绿零失败。合入。

@jackwener
jackwener merged commit ea36be4 into mainAug 7, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(settings): add a Projects page with a default project by jackwener · Pull Request #2446 · apache/maka · GitHub
Skip to content

feat(settings): add a Projects page with a default project - #2446

Merged
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page
Aug 7, 2026
Merged

feat(settings): add a Projects page with a default project#2446
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page

Conversation

@jackwener

Copy link
Copy Markdown
Member

改了什么

设置 · 偏好 新增「项目」页:项目列表管理 + 默认项目。task #160

为什么

项目管理原本散在各处——添加在 composer、重命名在侧栏行菜单,而「新对话从哪个项目打开」根本不是一个设置:它是「上次用过的那个」,由 project-root-controller 隐式决定,UI 上完全看不见也改不了。

三层规则(按优先级)

  1. composer 里显式选了项目 → 该次对话用它(每次对话都能临时覆盖
  2. 否则用设置里的默认项目
  3. 否则维持原来的 last-used 行为(没设默认的用户行为零变化

「默认覆盖 last-used」而不是「仅在没有 last-used 时兜底」:几乎总是有 last-used,兜底式默认等于永远不生效,那就又是一个说话不算话的旋钮。

两个设计决定

存 projectId 不存路径。 catalog 持有 identity,项目 relink 到新位置后 id 不变;存路径会让这个偏好在目录移动后静默指向别处。

不可用项目不能设为默认(按钮禁用,且禁用态自述原因)。#158「任何连接都能设默认」不同:连接失败是单次使用时才暴露、用户可以先设后修;而默认项目指向一个打不开的目录 = 每个新对话都坏,且修复手段(恢复目录)不在 app 内。预防优于降级。

默认项目失效(归档/目录没了)时,回退到 last-used 而不是让建会话失败,并且在设置页把降级说出来——静默降级和假控件是同一类谎。

范围说明

重命名在访达中打开 这一版没进行尾 … 菜单:前者需要仓库里尚不存在的 inline-edit 原语,后者需要新 IPC(app:openPath 只认「当前项目」)。两个能力目前都还在侧栏行菜单里,没有任何能力丢失,作为后续小任务补。

怎么验证的

  • 新增 4 个 main 侧解析测试:默认覆盖 last-used、显式选择覆盖默认、未设默认行为不变、默认失效时回退不炸。故障注入:把默认读取拆掉 → 恰好 1 个测试失败 → 装回全绿。
  • 新增 e2esettings-projects.spec.ts + settings-projects fixture(三个 catalog 条目:可用 / 可用长路径 / 目录真的不存在):列表渲染、不可用行显示「目录不可用」、禁用按钮的 aria-describedby 确实说明原因、设为默认后 Badge 互换且落盘、同时只有一个默认。
  • 新增 3 个 core 归一化测试:未设即无偏好、已设存活、空白 id 丢弃。
  • typecheck / lint / format / build / knip(两个 workspace)全绿;desktop 1751、core 812 全过。
  • 全量测试的既有失败 3 例(maka run process contract ×2、node:sqlite ×1)已确认为存量;runtime-host 那例是 Timing-sensitive tests fail under parallel workspace runner (concurrency=3) #2132 的负载下时序 flake,单跑 725/0 通过。

视觉

预览截图已过审(列表态 / 设为默认态)。截图过程中修掉三处:页头与节头「项目」标题重复、fixture 用编造路径导致整列渲染成不可用态、以及一个匹配不到 Astryx Item 的选择器。

Project management was spread across whichever surface happened to need it —
adding lived in the composer, renaming in the sidebar row menu — and "which
project does a new conversation open in" was not a setting at all. It was
whichever project you used last, decided implicitly by
`project-root-controller` and invisible from the UI.
Settings · 偏好 · 项目 makes the catalog a first-class surface and gives the
default a home. Three layers, in order: an explicit pick in the composer wins
for that conversation; otherwise the configured default; otherwise the
previous last-used behaviour, unchanged for anyone who sets nothing.
The default is stored as a project id rather than a path, because the catalog
owns identity — a relinked project keeps its id, so a stored path would
silently point somewhere else after a move. A default that no longer resolves
(archived, or the folder is gone) falls back to last-used rather than failing
session creation, and the page says so instead of degrading silently.
Unavailable projects cannot be made the default at all: unlike a model
connection, where a failure shows up per use and the user can fix it after
choosing, a default pointing at a missing folder breaks every new conversation
and the repair is outside the app. The disabled control states that reason
itself.
Rename and reveal-in-folder are not in the row menu yet — one needs an
inline-edit primitive this tree does not have, the other an IPC that can open
a path other than the current project's. Both still work from the sidebar row
menu, so no capability is lost.
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:项目设置页全案落地——预览阶段过审的结构(页头/描述行三层规则/行式方言/不可用态)+ 解析逻辑插在 resolveDesktopSessionSelection 的天然三层接缝(composer 单次覆盖免额外判断);「默认胜 last-used」测试带存在理由注释;降级可见且不把账算到新建对话头上;禁用态自述补齐并以 aria-describedby 实证(title 空断言的空过教训留档);单一默认的移动语义有 e2e 钉住。重命名/访达打开按批准降级为后续任务。CI 12 项全绿零失败。合入。

@jackwener
jackwener merged commit ea36be4 into mainAug 7, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(settings): add a Projects page with a default project by jackwener · Pull Request #2446 · apache/maka · GitHub
Skip to content

feat(settings): add a Projects page with a default project - #2446

Merged
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page
Aug 7, 2026
Merged

feat(settings): add a Projects page with a default project#2446
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page

Conversation

@jackwener

Copy link
Copy Markdown
Member

改了什么

设置 · 偏好 新增「项目」页:项目列表管理 + 默认项目。task #160

为什么

项目管理原本散在各处——添加在 composer、重命名在侧栏行菜单,而「新对话从哪个项目打开」根本不是一个设置:它是「上次用过的那个」,由 project-root-controller 隐式决定,UI 上完全看不见也改不了。

三层规则(按优先级)

  1. composer 里显式选了项目 → 该次对话用它(每次对话都能临时覆盖
  2. 否则用设置里的默认项目
  3. 否则维持原来的 last-used 行为(没设默认的用户行为零变化

「默认覆盖 last-used」而不是「仅在没有 last-used 时兜底」:几乎总是有 last-used,兜底式默认等于永远不生效,那就又是一个说话不算话的旋钮。

两个设计决定

存 projectId 不存路径。 catalog 持有 identity,项目 relink 到新位置后 id 不变;存路径会让这个偏好在目录移动后静默指向别处。

不可用项目不能设为默认(按钮禁用,且禁用态自述原因)。#158「任何连接都能设默认」不同:连接失败是单次使用时才暴露、用户可以先设后修;而默认项目指向一个打不开的目录 = 每个新对话都坏,且修复手段(恢复目录)不在 app 内。预防优于降级。

默认项目失效(归档/目录没了)时,回退到 last-used 而不是让建会话失败,并且在设置页把降级说出来——静默降级和假控件是同一类谎。

范围说明

重命名在访达中打开 这一版没进行尾 … 菜单:前者需要仓库里尚不存在的 inline-edit 原语,后者需要新 IPC(app:openPath 只认「当前项目」)。两个能力目前都还在侧栏行菜单里,没有任何能力丢失,作为后续小任务补。

怎么验证的

  • 新增 4 个 main 侧解析测试:默认覆盖 last-used、显式选择覆盖默认、未设默认行为不变、默认失效时回退不炸。故障注入:把默认读取拆掉 → 恰好 1 个测试失败 → 装回全绿。
  • 新增 e2esettings-projects.spec.ts + settings-projects fixture(三个 catalog 条目:可用 / 可用长路径 / 目录真的不存在):列表渲染、不可用行显示「目录不可用」、禁用按钮的 aria-describedby 确实说明原因、设为默认后 Badge 互换且落盘、同时只有一个默认。
  • 新增 3 个 core 归一化测试:未设即无偏好、已设存活、空白 id 丢弃。
  • typecheck / lint / format / build / knip(两个 workspace)全绿;desktop 1751、core 812 全过。
  • 全量测试的既有失败 3 例(maka run process contract ×2、node:sqlite ×1)已确认为存量;runtime-host 那例是 Timing-sensitive tests fail under parallel workspace runner (concurrency=3) #2132 的负载下时序 flake,单跑 725/0 通过。

视觉

预览截图已过审(列表态 / 设为默认态)。截图过程中修掉三处:页头与节头「项目」标题重复、fixture 用编造路径导致整列渲染成不可用态、以及一个匹配不到 Astryx Item 的选择器。

Project management was spread across whichever surface happened to need it —
adding lived in the composer, renaming in the sidebar row menu — and "which
project does a new conversation open in" was not a setting at all. It was
whichever project you used last, decided implicitly by
`project-root-controller` and invisible from the UI.
Settings · 偏好 · 项目 makes the catalog a first-class surface and gives the
default a home. Three layers, in order: an explicit pick in the composer wins
for that conversation; otherwise the configured default; otherwise the
previous last-used behaviour, unchanged for anyone who sets nothing.
The default is stored as a project id rather than a path, because the catalog
owns identity — a relinked project keeps its id, so a stored path would
silently point somewhere else after a move. A default that no longer resolves
(archived, or the folder is gone) falls back to last-used rather than failing
session creation, and the page says so instead of degrading silently.
Unavailable projects cannot be made the default at all: unlike a model
connection, where a failure shows up per use and the user can fix it after
choosing, a default pointing at a missing folder breaks every new conversation
and the repair is outside the app. The disabled control states that reason
itself.
Rename and reveal-in-folder are not in the row menu yet — one needs an
inline-edit primitive this tree does not have, the other an IPC that can open
a path other than the current project's. Both still work from the sidebar row
menu, so no capability is lost.
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:项目设置页全案落地——预览阶段过审的结构(页头/描述行三层规则/行式方言/不可用态)+ 解析逻辑插在 resolveDesktopSessionSelection 的天然三层接缝(composer 单次覆盖免额外判断);「默认胜 last-used」测试带存在理由注释;降级可见且不把账算到新建对话头上;禁用态自述补齐并以 aria-describedby 实证(title 空断言的空过教训留档);单一默认的移动语义有 e2e 钉住。重命名/访达打开按批准降级为后续任务。CI 12 项全绿零失败。合入。

@jackwener
jackwener merged commit ea36be4 into mainAug 7, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(settings): add a Projects page with a default project by jackwener · Pull Request #2446 · apache/maka · GitHub
Skip to content

feat(settings): add a Projects page with a default project - #2446

Merged
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page
Aug 7, 2026
Merged

feat(settings): add a Projects page with a default project#2446
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page

Conversation

@jackwener

Copy link
Copy Markdown
Member

改了什么

设置 · 偏好 新增「项目」页:项目列表管理 + 默认项目。task #160

为什么

项目管理原本散在各处——添加在 composer、重命名在侧栏行菜单,而「新对话从哪个项目打开」根本不是一个设置:它是「上次用过的那个」,由 project-root-controller 隐式决定,UI 上完全看不见也改不了。

三层规则(按优先级)

  1. composer 里显式选了项目 → 该次对话用它(每次对话都能临时覆盖
  2. 否则用设置里的默认项目
  3. 否则维持原来的 last-used 行为(没设默认的用户行为零变化

「默认覆盖 last-used」而不是「仅在没有 last-used 时兜底」:几乎总是有 last-used,兜底式默认等于永远不生效,那就又是一个说话不算话的旋钮。

两个设计决定

存 projectId 不存路径。 catalog 持有 identity,项目 relink 到新位置后 id 不变;存路径会让这个偏好在目录移动后静默指向别处。

不可用项目不能设为默认(按钮禁用,且禁用态自述原因)。#158「任何连接都能设默认」不同:连接失败是单次使用时才暴露、用户可以先设后修;而默认项目指向一个打不开的目录 = 每个新对话都坏,且修复手段(恢复目录)不在 app 内。预防优于降级。

默认项目失效(归档/目录没了)时,回退到 last-used 而不是让建会话失败,并且在设置页把降级说出来——静默降级和假控件是同一类谎。

范围说明

重命名在访达中打开 这一版没进行尾 … 菜单:前者需要仓库里尚不存在的 inline-edit 原语,后者需要新 IPC(app:openPath 只认「当前项目」)。两个能力目前都还在侧栏行菜单里,没有任何能力丢失,作为后续小任务补。

怎么验证的

  • 新增 4 个 main 侧解析测试:默认覆盖 last-used、显式选择覆盖默认、未设默认行为不变、默认失效时回退不炸。故障注入:把默认读取拆掉 → 恰好 1 个测试失败 → 装回全绿。
  • 新增 e2esettings-projects.spec.ts + settings-projects fixture(三个 catalog 条目:可用 / 可用长路径 / 目录真的不存在):列表渲染、不可用行显示「目录不可用」、禁用按钮的 aria-describedby 确实说明原因、设为默认后 Badge 互换且落盘、同时只有一个默认。
  • 新增 3 个 core 归一化测试:未设即无偏好、已设存活、空白 id 丢弃。
  • typecheck / lint / format / build / knip(两个 workspace)全绿;desktop 1751、core 812 全过。
  • 全量测试的既有失败 3 例(maka run process contract ×2、node:sqlite ×1)已确认为存量;runtime-host 那例是 Timing-sensitive tests fail under parallel workspace runner (concurrency=3) #2132 的负载下时序 flake,单跑 725/0 通过。

视觉

预览截图已过审(列表态 / 设为默认态)。截图过程中修掉三处:页头与节头「项目」标题重复、fixture 用编造路径导致整列渲染成不可用态、以及一个匹配不到 Astryx Item 的选择器。

Project management was spread across whichever surface happened to need it —
adding lived in the composer, renaming in the sidebar row menu — and "which
project does a new conversation open in" was not a setting at all. It was
whichever project you used last, decided implicitly by
`project-root-controller` and invisible from the UI.
Settings · 偏好 · 项目 makes the catalog a first-class surface and gives the
default a home. Three layers, in order: an explicit pick in the composer wins
for that conversation; otherwise the configured default; otherwise the
previous last-used behaviour, unchanged for anyone who sets nothing.
The default is stored as a project id rather than a path, because the catalog
owns identity — a relinked project keeps its id, so a stored path would
silently point somewhere else after a move. A default that no longer resolves
(archived, or the folder is gone) falls back to last-used rather than failing
session creation, and the page says so instead of degrading silently.
Unavailable projects cannot be made the default at all: unlike a model
connection, where a failure shows up per use and the user can fix it after
choosing, a default pointing at a missing folder breaks every new conversation
and the repair is outside the app. The disabled control states that reason
itself.
Rename and reveal-in-folder are not in the row menu yet — one needs an
inline-edit primitive this tree does not have, the other an IPC that can open
a path other than the current project's. Both still work from the sidebar row
menu, so no capability is lost.
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:项目设置页全案落地——预览阶段过审的结构(页头/描述行三层规则/行式方言/不可用态)+ 解析逻辑插在 resolveDesktopSessionSelection 的天然三层接缝(composer 单次覆盖免额外判断);「默认胜 last-used」测试带存在理由注释;降级可见且不把账算到新建对话头上;禁用态自述补齐并以 aria-describedby 实证(title 空断言的空过教训留档);单一默认的移动语义有 e2e 钉住。重命名/访达打开按批准降级为后续任务。CI 12 项全绿零失败。合入。

@jackwener
jackwener merged commit ea36be4 into mainAug 7, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(settings): add a Projects page with a default project by jackwener · Pull Request #2446 · apache/maka · GitHub
Skip to content

feat(settings): add a Projects page with a default project - #2446

Merged
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page
Aug 7, 2026
Merged

feat(settings): add a Projects page with a default project#2446
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page

Conversation

@jackwener

Copy link
Copy Markdown
Member

改了什么

设置 · 偏好 新增「项目」页:项目列表管理 + 默认项目。task #160

为什么

项目管理原本散在各处——添加在 composer、重命名在侧栏行菜单,而「新对话从哪个项目打开」根本不是一个设置:它是「上次用过的那个」,由 project-root-controller 隐式决定,UI 上完全看不见也改不了。

三层规则(按优先级)

  1. composer 里显式选了项目 → 该次对话用它(每次对话都能临时覆盖
  2. 否则用设置里的默认项目
  3. 否则维持原来的 last-used 行为(没设默认的用户行为零变化

「默认覆盖 last-used」而不是「仅在没有 last-used 时兜底」:几乎总是有 last-used,兜底式默认等于永远不生效,那就又是一个说话不算话的旋钮。

两个设计决定

存 projectId 不存路径。 catalog 持有 identity,项目 relink 到新位置后 id 不变;存路径会让这个偏好在目录移动后静默指向别处。

不可用项目不能设为默认(按钮禁用,且禁用态自述原因)。#158「任何连接都能设默认」不同:连接失败是单次使用时才暴露、用户可以先设后修;而默认项目指向一个打不开的目录 = 每个新对话都坏,且修复手段(恢复目录)不在 app 内。预防优于降级。

默认项目失效(归档/目录没了)时,回退到 last-used 而不是让建会话失败,并且在设置页把降级说出来——静默降级和假控件是同一类谎。

范围说明

重命名在访达中打开 这一版没进行尾 … 菜单:前者需要仓库里尚不存在的 inline-edit 原语,后者需要新 IPC(app:openPath 只认「当前项目」)。两个能力目前都还在侧栏行菜单里,没有任何能力丢失,作为后续小任务补。

怎么验证的

  • 新增 4 个 main 侧解析测试:默认覆盖 last-used、显式选择覆盖默认、未设默认行为不变、默认失效时回退不炸。故障注入:把默认读取拆掉 → 恰好 1 个测试失败 → 装回全绿。
  • 新增 e2esettings-projects.spec.ts + settings-projects fixture(三个 catalog 条目:可用 / 可用长路径 / 目录真的不存在):列表渲染、不可用行显示「目录不可用」、禁用按钮的 aria-describedby 确实说明原因、设为默认后 Badge 互换且落盘、同时只有一个默认。
  • 新增 3 个 core 归一化测试:未设即无偏好、已设存活、空白 id 丢弃。
  • typecheck / lint / format / build / knip(两个 workspace)全绿;desktop 1751、core 812 全过。
  • 全量测试的既有失败 3 例(maka run process contract ×2、node:sqlite ×1)已确认为存量;runtime-host 那例是 Timing-sensitive tests fail under parallel workspace runner (concurrency=3) #2132 的负载下时序 flake,单跑 725/0 通过。

视觉

预览截图已过审(列表态 / 设为默认态)。截图过程中修掉三处:页头与节头「项目」标题重复、fixture 用编造路径导致整列渲染成不可用态、以及一个匹配不到 Astryx Item 的选择器。

Project management was spread across whichever surface happened to need it —
adding lived in the composer, renaming in the sidebar row menu — and "which
project does a new conversation open in" was not a setting at all. It was
whichever project you used last, decided implicitly by
`project-root-controller` and invisible from the UI.
Settings · 偏好 · 项目 makes the catalog a first-class surface and gives the
default a home. Three layers, in order: an explicit pick in the composer wins
for that conversation; otherwise the configured default; otherwise the
previous last-used behaviour, unchanged for anyone who sets nothing.
The default is stored as a project id rather than a path, because the catalog
owns identity — a relinked project keeps its id, so a stored path would
silently point somewhere else after a move. A default that no longer resolves
(archived, or the folder is gone) falls back to last-used rather than failing
session creation, and the page says so instead of degrading silently.
Unavailable projects cannot be made the default at all: unlike a model
connection, where a failure shows up per use and the user can fix it after
choosing, a default pointing at a missing folder breaks every new conversation
and the repair is outside the app. The disabled control states that reason
itself.
Rename and reveal-in-folder are not in the row menu yet — one needs an
inline-edit primitive this tree does not have, the other an IPC that can open
a path other than the current project's. Both still work from the sidebar row
menu, so no capability is lost.
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:项目设置页全案落地——预览阶段过审的结构(页头/描述行三层规则/行式方言/不可用态)+ 解析逻辑插在 resolveDesktopSessionSelection 的天然三层接缝(composer 单次覆盖免额外判断);「默认胜 last-used」测试带存在理由注释;降级可见且不把账算到新建对话头上;禁用态自述补齐并以 aria-describedby 实证(title 空断言的空过教训留档);单一默认的移动语义有 e2e 钉住。重命名/访达打开按批准降级为后续任务。CI 12 项全绿零失败。合入。

@jackwener
jackwener merged commit ea36be4 into mainAug 7, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(settings): add a Projects page with a default project by jackwener · Pull Request #2446 · apache/maka · GitHub
Skip to content

feat(settings): add a Projects page with a default project - #2446

Merged
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page
Aug 7, 2026
Merged

feat(settings): add a Projects page with a default project#2446
jackwener merged 1 commit into
mainfrom
feat/projects-settings-page

Conversation

@jackwener

Copy link
Copy Markdown
Member

改了什么

设置 · 偏好 新增「项目」页:项目列表管理 + 默认项目。task #160

为什么

项目管理原本散在各处——添加在 composer、重命名在侧栏行菜单,而「新对话从哪个项目打开」根本不是一个设置:它是「上次用过的那个」,由 project-root-controller 隐式决定,UI 上完全看不见也改不了。

三层规则(按优先级)

  1. composer 里显式选了项目 → 该次对话用它(每次对话都能临时覆盖
  2. 否则用设置里的默认项目
  3. 否则维持原来的 last-used 行为(没设默认的用户行为零变化

「默认覆盖 last-used」而不是「仅在没有 last-used 时兜底」:几乎总是有 last-used,兜底式默认等于永远不生效,那就又是一个说话不算话的旋钮。

两个设计决定

存 projectId 不存路径。 catalog 持有 identity,项目 relink 到新位置后 id 不变;存路径会让这个偏好在目录移动后静默指向别处。

不可用项目不能设为默认(按钮禁用,且禁用态自述原因)。#158「任何连接都能设默认」不同:连接失败是单次使用时才暴露、用户可以先设后修;而默认项目指向一个打不开的目录 = 每个新对话都坏,且修复手段(恢复目录)不在 app 内。预防优于降级。

默认项目失效(归档/目录没了)时,回退到 last-used 而不是让建会话失败,并且在设置页把降级说出来——静默降级和假控件是同一类谎。

范围说明

重命名在访达中打开 这一版没进行尾 … 菜单:前者需要仓库里尚不存在的 inline-edit 原语,后者需要新 IPC(app:openPath 只认「当前项目」)。两个能力目前都还在侧栏行菜单里,没有任何能力丢失,作为后续小任务补。

怎么验证的

  • 新增 4 个 main 侧解析测试:默认覆盖 last-used、显式选择覆盖默认、未设默认行为不变、默认失效时回退不炸。故障注入:把默认读取拆掉 → 恰好 1 个测试失败 → 装回全绿。
  • 新增 e2esettings-projects.spec.ts + settings-projects fixture(三个 catalog 条目:可用 / 可用长路径 / 目录真的不存在):列表渲染、不可用行显示「目录不可用」、禁用按钮的 aria-describedby 确实说明原因、设为默认后 Badge 互换且落盘、同时只有一个默认。
  • 新增 3 个 core 归一化测试:未设即无偏好、已设存活、空白 id 丢弃。
  • typecheck / lint / format / build / knip(两个 workspace)全绿;desktop 1751、core 812 全过。
  • 全量测试的既有失败 3 例(maka run process contract ×2、node:sqlite ×1)已确认为存量;runtime-host 那例是 Timing-sensitive tests fail under parallel workspace runner (concurrency=3) #2132 的负载下时序 flake,单跑 725/0 通过。

视觉

预览截图已过审(列表态 / 设为默认态)。截图过程中修掉三处:页头与节头「项目」标题重复、fixture 用编造路径导致整列渲染成不可用态、以及一个匹配不到 Astryx Item 的选择器。

Project management was spread across whichever surface happened to need it —
adding lived in the composer, renaming in the sidebar row menu — and "which
project does a new conversation open in" was not a setting at all. It was
whichever project you used last, decided implicitly by
`project-root-controller` and invisible from the UI.
Settings · 偏好 · 项目 makes the catalog a first-class surface and gives the
default a home. Three layers, in order: an explicit pick in the composer wins
for that conversation; otherwise the configured default; otherwise the
previous last-used behaviour, unchanged for anyone who sets nothing.
The default is stored as a project id rather than a path, because the catalog
owns identity — a relinked project keeps its id, so a stored path would
silently point somewhere else after a move. A default that no longer resolves
(archived, or the folder is gone) falls back to last-used rather than failing
session creation, and the page says so instead of degrading silently.
Unavailable projects cannot be made the default at all: unlike a model
connection, where a failure shows up per use and the user can fix it after
choosing, a default pointing at a missing folder breaks every new conversation
and the repair is outside the app. The disabled control states that reason
itself.
Rename and reveal-in-folder are not in the row menu yet — one needs an
inline-edit primitive this tree does not have, the other an IPC that can open
a path other than the current project's. Both still work from the sidebar row
menu, so no capability is lost.
@jackwener

Copy link
Copy Markdown
MemberAuthor

Review by maka-审美专家 — 通过:项目设置页全案落地——预览阶段过审的结构(页头/描述行三层规则/行式方言/不可用态)+ 解析逻辑插在 resolveDesktopSessionSelection 的天然三层接缝(composer 单次覆盖免额外判断);「默认胜 last-used」测试带存在理由注释;降级可见且不把账算到新建对话头上;禁用态自述补齐并以 aria-describedby 实证(title 空断言的空过教训留档);单一默认的移动语义有 e2e 钉住。重命名/访达打开按批准降级为后续任务。CI 12 项全绿零失败。合入。

@jackwener
jackwener merged commit ea36be4 into mainAug 7, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jackwener