Compare commits

..
Author SHA1 Message Date
Peter Steinberger 3ac064139d fix: publish multi-arch docker images (#1602) (thanks @denysvitali) 2026-01-24 19:23:09 +00:00
Denys Vitali 2045812a65 Simplify Docker release workflow by removing submodule checkout
Removed submodule checkout step from Docker release workflow.
2026-01-24 19:23:09 +00:00
Denys Vitali fe3dd0c5e3 Remove submodule checkout from docker-release.yml
Removed submodule checkout step from Docker release workflow.
2026-01-24 19:23:09 +00:00
Denys VitaliandClaude 1e7737b732 ci: sync docker-release workflow updates
Squashes:
- ci: use correct runs-on
- ci: build images

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-24 19:23:09 +00:00
Denys Vitali 41929129ce ci: build & release docker image 2026-01-24 19:23:09 +00:00
8 changed files with 71 additions and 136 deletions

No files matched your search

+6 -10
View File
@@ -41,12 +41,8 @@ jobs:
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{version}},suffix=-amd64
type=semver,pattern={{version}},suffix=-arm64
type=ref,event=branch,suffix=-amd64
type=ref,event=branch,suffix=-arm64
- name: Build and push amd64 image
id: build
@@ -90,11 +86,7 @@ jobs:
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{version}},suffix=-amd64
type=semver,pattern={{version}},suffix=-arm64
type=ref,event=branch,suffix=-amd64
type=ref,event=branch,suffix=-arm64
- name: Build and push arm64 image
@@ -118,6 +110,9 @@ jobs:
contents: read
needs: [build-amd64, build-arm64]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
@@ -133,11 +128,12 @@ jobs:
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=raw,value=latest,enable={{is_tag}}
- name: Create and push manifest
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
${{ needs.build-amd64.outputs.image-digest }} \
${{ needs.build-arm64.outputs.image-digest }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-amd64.outputs.image-digest }} \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-arm64.outputs.image-digest }}
env:
DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }}
+1 -1
View File
@@ -12,12 +12,12 @@ Docs: https://docs.clawd.bot
### Fixes
- Web UI: hide internal `message_id` hints in chat bubbles.
- Heartbeat: normalize target identifiers for consistent routing.
- TUI: unify reasoning tag stripping so `<final>` wrappers stay hidden. (#1613) Thanks @kyleok.
## 2026.1.23-1
### Fixes
- Packaging: include dist/tts output in npm tarball (fixes missing dist/tts/tts.js).
- CI: publish multi-arch Docker images on main and release tags. (#1602) Thanks @denysvitali.
## 2026.1.23
-16
View File
@@ -460,22 +460,6 @@ File contents here`,
expect(result).toBe("The actual answer.");
});
it("strips final tags while keeping content", () => {
const msg: AssistantMessage = {
role: "assistant",
content: [
{
type: "text",
text: "<final>\nAnswer\n</final>",
},
],
timestamp: Date.now(),
};
const result = extractAssistantText(msg);
expect(result).toBe("Answer");
});
it("strips thought tags", () => {
const msg: AssistantMessage = {
role: "assistant",
+30 -2
View File
@@ -1,5 +1,4 @@
import type { AssistantMessage } from "@mariozechner/pi-ai";
import { stripReasoningTagsFromText } from "../shared/text/reasoning-tags.js";
import { sanitizeUserFacingText } from "./pi-embedded-helpers.js";
import { formatToolDetail, resolveToolDisplay } from "./tool-display.js";
@@ -167,7 +166,36 @@ export function stripDowngradedToolCallText(text: string): string {
* that slip through other filtering mechanisms.
*/
export function stripThinkingTagsFromText(text: string): string {
return stripReasoningTagsFromText(text, { mode: "strict", trim: "both" });
if (!text) return text;
// Quick check to avoid regex overhead when no tags present.
if (!/(?:think(?:ing)?|thought|antthinking)/i.test(text)) return text;
const tagRe = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^>]*>/gi;
let result = "";
let lastIndex = 0;
let inThinking = false;
for (const match of text.matchAll(tagRe)) {
const idx = match.index ?? 0;
const isClose = match[1] === "/";
if (!inThinking && !isClose) {
// Opening tag - save text before it.
result += text.slice(lastIndex, idx);
inThinking = true;
} else if (inThinking && isClose) {
// Closing tag - skip content inside.
inThinking = false;
}
lastIndex = idx + match[0].length;
}
// Append remaining text if we're not inside thinking.
if (!inThinking) {
result += text.slice(lastIndex);
}
return result.trim();
}
export function extractAssistantText(msg: AssistantMessage): string {
+1 -2
View File
@@ -67,10 +67,9 @@ export function normalizeAgentId(value: string | undefined | null): string {
export function sanitizeAgentId(value: string | undefined | null): string {
const trimmed = (value ?? "").trim();
if (!trimmed) return DEFAULT_AGENT_ID;
if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed.toLowerCase();
if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed;
return (
trimmed
.toLowerCase()
.replace(/[^a-z0-9_-]+/gi, "-")
.replace(/^-+/, "")
.replace(/-+$/, "")
-61
View File
@@ -1,61 +0,0 @@
export type ReasoningTagMode = "strict" | "preserve";
export type ReasoningTagTrim = "none" | "start" | "both";
const QUICK_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking|final)\b/i;
const FINAL_TAG_RE = /<\s*\/?\s*final\b[^>]*>/gi;
const THINKING_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^>]*>/gi;
function applyTrim(value: string, mode: ReasoningTagTrim): string {
if (mode === "none") return value;
if (mode === "start") return value.trimStart();
return value.trim();
}
export function stripReasoningTagsFromText(
text: string,
options?: {
mode?: ReasoningTagMode;
trim?: ReasoningTagTrim;
},
): string {
if (!text) return text;
if (!QUICK_TAG_RE.test(text)) return text;
const mode = options?.mode ?? "strict";
const trimMode = options?.trim ?? "both";
let cleaned = text;
if (FINAL_TAG_RE.test(cleaned)) {
FINAL_TAG_RE.lastIndex = 0;
cleaned = cleaned.replace(FINAL_TAG_RE, "");
} else {
FINAL_TAG_RE.lastIndex = 0;
}
THINKING_TAG_RE.lastIndex = 0;
let result = "";
let lastIndex = 0;
let inThinking = false;
for (const match of cleaned.matchAll(THINKING_TAG_RE)) {
const idx = match.index ?? 0;
const isClose = match[1] === "/";
if (!inThinking) {
result += cleaned.slice(lastIndex, idx);
if (!isClose) {
inThinking = true;
}
} else if (isClose) {
inThinking = false;
}
lastIndex = idx + match[0].length;
}
if (!inThinking || mode === "preserve") {
result += cleaned.slice(lastIndex);
}
return applyTrim(result, trimMode);
}
-41
View File
@@ -21,46 +21,5 @@ describe("stripThinkingTags", () => {
it("returns original text when no tags exist", () => {
expect(stripThinkingTags("Hello")).toBe("Hello");
});
<<<<<<< fix/tui-final-tag-strip
it("strips <final>…</final> segments", () => {
const input = "<final>\n\nHello there\n\n</final>";
expect(stripThinkingTags(input)).toBe("Hello there\n\n");
});
it("strips mixed <think> and <final> tags", () => {
const input = "<think>reasoning</think>\n\n<final>Hello</final>";
expect(stripThinkingTags(input)).toBe("Hello");
});
it("handles incomplete <final tag gracefully", () => {
// When streaming splits mid-tag, we may see "<final" without closing ">"
// This should not crash and should handle gracefully
expect(stripThinkingTags("<final\nHello")).toBe("<final\nHello");
expect(stripThinkingTags("Hello</final>")).toBe("Hello");
});
});
||||||| temp/landpr-
});
=======
>>>>>>> local
it("strips <final>…</final> segments while keeping content", () => {
const input = "<final>\n\nHello there\n\n</final>";
expect(stripThinkingTags(input)).toBe("Hello there\n\n");
});
it("strips mixed <think> and <final> tags", () => {
const input = "<think>reasoning</think>\n\n<final>Hello</final>";
expect(stripThinkingTags(input)).toBe("Hello");
});
it("strips unpaired <final> tags", () => {
expect(stripThinkingTags("<final>Hello")).toBe("Hello");
expect(stripThinkingTags("Hello</final>")).toBe("Hello");
});
it("keeps incomplete <final tag text", () => {
expect(stripThinkingTags("<final\nHello")).toBe("<final\nHello");
});
});
+33 -3
View File
@@ -1,5 +1,3 @@
import { stripReasoningTagsFromText } from "../../../src/shared/text/reasoning-tags.js";
export function formatMs(ms?: number | null): string {
if (!ms && ms !== 0) return "n/a";
return new Date(ms).toLocaleString();
@@ -69,6 +67,38 @@ export function parseList(input: string): string[] {
.filter((v) => v.length > 0);
}
const THINKING_TAG_RE = /<\s*\/?\s*think(?:ing)?\s*>/gi;
const THINKING_OPEN_RE = /<\s*think(?:ing)?\s*>/i;
const THINKING_CLOSE_RE = /<\s*\/\s*think(?:ing)?\s*>/i;
export function stripThinkingTags(value: string): string {
return stripReasoningTagsFromText(value, { mode: "preserve", trim: "start" });
if (!value) return value;
const hasOpen = THINKING_OPEN_RE.test(value);
const hasClose = THINKING_CLOSE_RE.test(value);
if (!hasOpen && !hasClose) return value;
// If we don't have a balanced pair, avoid dropping trailing content.
if (hasOpen !== hasClose) {
if (!hasOpen) return value.replace(THINKING_CLOSE_RE, "").trimStart();
return value.replace(THINKING_OPEN_RE, "").trimStart();
}
if (!THINKING_TAG_RE.test(value)) return value;
THINKING_TAG_RE.lastIndex = 0;
let result = "";
let lastIndex = 0;
let inThinking = false;
for (const match of value.matchAll(THINKING_TAG_RE)) {
const idx = match.index ?? 0;
if (!inThinking) {
result += value.slice(lastIndex, idx);
}
const tag = match[0].toLowerCase();
inThinking = !tag.includes("/");
lastIndex = idx + match[0].length;
}
if (!inThinking) {
result += value.slice(lastIndex);
}
return result.trimStart();
}